Developing Molds
This guide walks you through the full workflow for building, testing, and publishing molds. If you haven’t read the mold reference yet, start with Creating Molds for the YAML schema and Forge Actions for the full action catalog.
Prerequisites
Section titled “Prerequisites”Before you start:
- Shoehorn CLI installed (
shoehorn versionworks) - Authenticated to your Shoehorn instance (
shoehorn auth login) - A GitHub repository connected to Shoehorn (the crawler discovers molds from
.shoehorn/molds/) - Forge GitHub App configured for repository creation, file writes, pull requests, and access changes
Quick start
Section titled “Quick start”# 1. Create the molds directory in your repomkdir -p .shoehorn/molds
# 2. Write a moldcat > .shoehorn/molds/create-repo.yaml << 'EOF'version: "1.0.0"
metadata: name: create-repo displayName: Create Repository description: Create a new GitHub repository author: my-team icon: git-branch category: repository tags: - github
inputs: type: object required: - name - owner properties: name: type: string title: Repository Name pattern: "^[a-zA-Z0-9._-]+$" owner: type: string title: GitHub Organization private: type: boolean title: Private Repository
defaults: private: 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 }}"EOF
# 3. Validate the moldshoehorn validate mold .shoehorn/molds/create-repo.yaml
# 4. Commit and push (crawler will discover it)git add .shoehorn/molds/create-repo.yamlgit commit -m "feat: add create-repo mold"git push
# 5. Test with dry-run (no real resources created)shoehorn forge execute create-repo \ --input name=test-repo --input owner=my-org --dry-run
# 6. Execute for realshoehorn forge execute create-repo \ --input name=my-new-service --input owner=my-orgDevelopment workflow
Section titled “Development workflow”Step 1: Write the mold YAML
Section titled “Step 1: Write the mold YAML”Create a .yaml file under .shoehorn/molds/ in any repository connected to Shoehorn. The crawler watches for files matching .shoehorn/molds/**/*.{yaml,yml,json}.
Start with the minimal structure:
version: "1.0.0"
metadata: name: my-mold # unique slug -- this becomes the mold identifier displayName: My Mold description: What it does author: your-team icon: server # Lucide icon name category: repository # repository, service, infrastructure tags: []
inputs: type: object required: [] properties: {}
actions: - action: my.action.id label: Run primary: true
steps: []Then add inputs, steps, and actions as needed.
Step 2: Validate locally
Section titled “Step 2: Validate locally”Before pushing, run the offline validator:
shoehorn validate mold .shoehorn/molds/my-mold.yamlThis checks:
- YAML syntax is valid
- Required fields are present (
version,metadata.name,stepsoractions) - Each step has
id,name, and eitheractionoradapter - Action IDs follow the
provider.resource.operationformat - Adapter names are in the valid set
- Approval flow constraints (if defined)
- Input schema structure
Fix any errors before pushing.
Step 3: Push and let the crawler discover it
Section titled “Step 3: Push and let the crawler discover it”Commit and push. The crawler indexes .shoehorn/molds/ files from connected repositories. Once discovered, the mold appears in Forge within the next crawl cycle (usually a few minutes).
git add .shoehorn/molds/my-mold.yamlgit commit -m "feat: add my-mold"git pushCheck that the crawler found it:
shoehorn forge molds listshoehorn forge molds get my-moldStep 4: Test with dry-run
Section titled “Step 4: Test with dry-run”Dry-run validates inputs and simulates execution without creating real resources:
# Via CLIshoehorn forge execute my-mold \ --input name=test --input owner=my-org --dry-run
# Via UI: toggle "Dry Run" in the run formDry-run checks:
- All required inputs are provided
- Input types match the schema
- The mold and action exist
- Idempotency key handling
It does not execute Forge actions or make GitHub API calls.
Step 5: Execute and verify
Section titled “Step 5: Execute and verify”Run for real against a test org or with disposable resources:
shoehorn forge execute my-mold \ --input name=test-deleteme --input owner=my-orgWatch the run:
shoehorn forge run watch <run-id>Check the result:
shoehorn forge run get <run-id>shoehorn forge run get <run-id> -o json | jq '.outputs'The documented Forge actions are idempotent, so it’s safe to re-run the same mold with the same inputs.
Step 6: Iterate
Section titled “Step 6: Iterate”Edit the YAML, push again, and the crawler updates the mold. The version field is informational — the crawler upserts by metadata.name (slug).
Common iteration patterns:
- Add more steps (files, topics, team access)
- Refine input validation (patterns, enums, min/max)
- Add conditional fields with
x-dependency - Add approval flow for production molds
- Add
catalog.entity.registerto auto-register the created resource
Choosing between action and workflow steps
Section titled “Choosing between action and workflow steps”Use Forge action steps (action:) when… | Use workflow steps (adapter:) when… |
|---|---|
| All operations are on GitHub | You need HTTP calls to external APIs |
| Sequential execution is fine | You need explicit depends_on ordering |
| You want idempotent actions | You need conditional execution (condition:) |
| You want the simplest YAML | You need catalog lookups or logging |
You can tell which format a mold uses by looking at the steps — action: means Forge action steps, adapter: means workflow steps. Don’t mix them in the same mold.
Expression reference (quick)
Section titled “Expression reference (quick)”| Syntax | Description |
|---|---|
${{ inputs.name }} | User-provided input value |
${{ parameters.name }} | Alias for inputs |
${{ steps.step-id.output.field }} | Output from a previous step |
${{ context.createdBy }} | Email of the user who triggered the run |
${{ context.tenantId }} | Tenant/organization ID |
${{ context.runId }} | Unique run ID |
${{ context.moldSlug }} | Slug of the mold being executed |
${{ secrets.KEY }} | Secret value (blocked in sandboxed mode) |
${{ env.KEY }} | Environment variable (blocked in sandboxed mode) |
Type preservation: "${{ inputs.private }}" as the entire value preserves the boolean. Mixed text like "repo-${{ inputs.name }}" coerces to string.
Input schema tips
Section titled “Input schema tips”Required vs optional
Section titled “Required vs optional”inputs: type: object required: - name # user must fill this in properties: name: type: string title: Name description: # optional -- not in required list type: string title: DescriptionDropdowns with enum
Section titled “Dropdowns with enum”goVersion: type: string title: Go Version enum: ["1.23", "1.24", "1.25"]Pattern validation
Section titled “Pattern validation”name: type: string title: Service Name pattern: "^[a-z0-9-]+$" minLength: 1 maxLength: 63 ui:options: placeholder: my-serviceConditional fields
Section titled “Conditional fields”Show a field only when another field has a certain value:
enableNotifications: type: boolean title: Enable Notifications
slackChannel: type: string title: Slack Channel x-dependency: condition: enableNotifications == trueDefaults
Section titled “Defaults”defaults: goVersion: "1.24" private: true replicas: 2Defaults are pre-filled in the UI form and applied automatically by the CLI.
Testing patterns
Section titled “Testing patterns”Disposable resources
Section titled “Disposable resources”Create test repos with a naming convention you can clean up later:
shoehorn forge execute my-mold \ --input name=test-deleteme-$(date +%s) --input owner=my-orgIdempotent re-runs
Section titled “Idempotent re-runs”The documented Forge actions are idempotent. Running the same mold twice with the same inputs returns the existing resources instead of failing. Use this to verify your mold is safe to retry.
Check outputs
Section titled “Check outputs”shoehorn forge run get <run-id> -o json | jq '.outputs'Verify in GitHub
Section titled “Verify in GitHub”After a successful run, check:
- Repository was created with correct settings
- Files were committed with correct content
- Topics were applied
- Team/collaborator access was set
Troubleshooting
Section titled “Troubleshooting”Mold not appearing in Forge
Section titled “Mold not appearing in Forge”- Check the file is under
.shoehorn/molds/(not.shoehorn/mold/or another path) - Check the file extension is
.yaml,.yml, or.json - Wait for the next crawler cycle (check crawler health:
curl http://localhost:8086/health) - Check crawler logs for parse errors on your file
”No action specified” error
Section titled “”No action specified” error”Your mold needs at least one entry in actions: with a matching step. If the CLI can’t auto-detect the primary action, pass --action explicitly.
”Missing required inputs” error
Section titled “”Missing required inputs” error”The CLI validates required inputs before creating a run. Pass all required fields via --input key=value.
Step fails with “action not found”
Section titled “Step fails with “action not found””Check the action ID uses dot notation (github.repo.create, not github:repo:create). The engine converts dots to colons internally.
Expression not resolving
Section titled “Expression not resolving”- Check the syntax:
${{ inputs.name }}(with spaces inside braces) - Check the namespace:
inputsorparametersfor user values,steps.id.output.fieldfor step outputs - Unresolved expressions are left as literal strings — look for
${{in your run outputs
Approval flow blocking runs
Section titled “Approval flow blocking runs”If your mold has approvalFlow.required: true, runs enter pending_approval status. Approvers must approve in the Forge UI before execution begins.
What’s next
Section titled “What’s next”- Forge Actions — full reference for all Forge actions and catalog-backed selectors
- Approval Workflows — multi-step approval chains
- CLI Reference — all CLI commands including forge