GitHub Issue Creation — Data-Driven Workflow Engine
This skill creates GitHub issues by dynamically fetching field definitions from GitHub issue templates at runtime. Template metadata (triggers, labels, defaults, formatting rules) is stored in per-template JSON config files. Configs can be stored locally or in a GitHub repository for cross-machine and team sharing. The skill itself contains no hardcoded field definitions.
File Structure
~/.claude/skills/github-issue-from-templates/
SKILL.md # This file — generic workflow engine
references/
schema.json # JSON Schema for template config files
settings-schema.json # JSON Schema for settings.json
~/.claude/configs/github-issue-from-templates/
settings.json # Storage mode config — created on first run
*.json # Template configs (local mode only) — user-managed, survives skill updates
<owner>/<repo>/<path>/ # Template configs (GitHub mode) — stored in a GitHub repo
*.json
Why a separate directory? The skill installation directory (
) is replaced on
. Storing template configs in
~/.claude/configs/github-issue-from-templates/
(local mode) or a GitHub repo (GitHub mode) keeps them safe across updates. The
file always lives locally since it tells the skill where to find configs.
Tool Detection
Before starting the workflow, determine which GitHub tool is available:
- GitHub MCP (preferred): Check if the GitHub MCP server is available by looking for MCP tools like or . If available, use MCP tools throughout.
- CLI (fallback): If GitHub MCP is not available, verify the CLI is installed and authenticated by running . If authenticated, use CLI commands throughout.
- Neither available: Notify the user that either the GitHub MCP server or the CLI is required, and stop.
Store the detected tool as the
GitHub method (
or
) and use it consistently for all GitHub operations in the workflow.
Note: When using GitHub repo storage, the same detected method is used for additional operations: listing directory contents, reading files, creating/updating files, and optionally creating repositories.
Workflow
Step 0: Settings & Storage Resolution
Before template selection, resolve where configs are stored.
- Check if
~/.claude/configs/github-issue-from-templates/settings.json
exists.
- If it exists: Read it, validate against
references/settings-schema.json
, and resolve the storage mode:
configStorage.type === "local"
→ configs are in ~/.claude/configs/github-issue-from-templates/
configStorage.type === "github"
→ configs are in the specified GitHub repo at the configured path and branch
- If it does not exist: Run the Setup Flow (see below).
Setup Flow (First Run)
Ask the user how they want to store their template configs:
Option A — Local storage:
- Create the directory
~/.claude/configs/github-issue-from-templates/
if it doesn't exist
- Write :
json
{
"configStorage": {
"type": "local"
}
}
- Offer to create a first template config
Option B — GitHub repository storage:
- Ask if they have an existing repo for configs
- If yes:
- Gather: , , (default:
configs/github-issue-from-templates/
), (default: )
- Validate access by listing the directory contents using the detected GitHub method (see Loading Configs from GitHub for commands)
- Write :
json
{
"configStorage": {
"type": "github",
"owner": "<owner>",
"repo": "<repo>",
"path": "configs/github-issue-from-templates/",
"branch": "main"
}
}
- If no — help create a new repo:
- Suggest the name
github-issue-from-templates-configs
(default private)
- If MCP: Use with , ,
- If CLI:
gh repo create <owner>/github-issue-from-templates-configs --private --description "Template configs for github-issue-from-templates skill"
- Create the initial directory by committing a placeholder at the configured path
- Write as above
Switching Storage Modes
If the user asks to change their storage mode (e.g., from local to GitHub or vice versa):
- Read the current to determine the current mode
- Ask the user if they want to migrate existing configs to the new location:
- Local → GitHub: Read each local config (excluding ), then commit each to the GitHub repo at the configured path
- GitHub → Local: Fetch each config from GitHub, then write each to
~/.claude/configs/github-issue-from-templates/
- Update with the new storage configuration
- Confirm the switch and report how many configs were migrated
Step 1: Template Selection
- Load configs based on the resolved storage mode from Step 0:
- Local: Read all files from
~/.claude/configs/github-issue-from-templates/
, excluding . If the directory does not exist or contains no config files, offer to create a first template config.
- GitHub: List and fetch files from the configured repo/path/branch (see Loading Configs from GitHub), excluding and . If the directory is empty or inaccessible, notify the user and offer to add a first config.
- For each config, compare the user's request against (case-insensitive substring match) and .
- Single match: Proceed with that template. Confirm the selection with the user briefly (e.g., "I'll use the [template name] template.").
- Multiple matches: Present the matching templates by and and ask the user to choose.
- No match: Present all available templates by and and ask the user to choose.
Loading Configs from GitHub
When
configStorage.type === "github"
, use these methods to list and fetch config files.
Listing files in the config directory
If MCP: Use
on the directory path:
owner: <configStorage.owner>
repo: <configStorage.repo>
path: <configStorage.path>
ref: <configStorage.branch>
The response returns an array of file entries. Filter to
files, excluding
.
If CLI: Use the GitHub contents API:
bash
gh api repos/<owner>/<repo>/contents/<path>?ref=<branch> --jq '.[] | select(.name | endswith(".json")) | select(.name != "settings.json") | .name'
Fetching individual config files
If MCP: Use
with the full file path:
owner: <configStorage.owner>
repo: <configStorage.repo>
path: <configStorage.path>/<filename>
ref: <configStorage.branch>
If CLI:
bash
gh api repos/<owner>/<repo>/contents/<path>/<filename>?ref=<branch> --jq '.content' | base64 -d
Parse each fetched file as JSON. Skip files that fail to parse and notify the user (see
Error Handling).
Step 2: Fetch Template from GitHub
Fetch the template file using the detected GitHub method:
owner: <config.repository.owner>
repo: <config.repository.repo>
path: <config.templateSource.path>
If CLI: Use
to fetch the raw file content:
bash
gh api repos/<config.repository.owner>/<config.repository.repo>/contents/<config.templateSource.path> --jq '.content' | base64 -d
Then parse based on
config.templateSource.format
:
Format: (Form-based templates)
Parse the YAML content and extract:
- Title pattern: From the top-level field (e.g.,
"[Issue Type] [Short descriptive title]"
)
- Template-level labels: From the top-level array
- Template-level assignees: From the top-level array
- Fields: From the array. For each entry:
- Skip entries where — these are instructional text, not fields
- For all other entries, extract:
- — unique field identifier
- — , ,
- — human-readable field name
- — help text for the field
- — available choices (for dropdowns)
- — example/guidance text
- — whether the field must be filled
Format: (Frontmatter + markdown templates)
Parse the frontmatter (between
delimiters) and extract:
- Title pattern: From (e.g.,
'[A11y]: Product - Feature - Request'
)
- Template-level labels: From (may be a string or array)
- Template-level assignees: From (may be a string or array)
Parse the markdown body to identify:
- Sections: headings define major sections
- Checkbox groups: Lines matching grouped under a heading or bold label
- Labeled fields: Bold-labeled list items like under a section
- Self-verification checklists: Sections like "Yes, I have" contain items the skill should satisfy automatically
Step 3: Gather Information
For each extracted field, apply the following logic in order:
-
Pre-fill from user request: If the user already provided a value for this field in their initial message, pre-fill it and confirm during the preview step.
-
Apply defaults: Check
config.fieldDefaults[fieldId].value
— if present, use as the default. Also check
for matching keys.
-
Check skip conditions: Check
config.fieldSkipConditions[fieldId]
— if an
condition exists and is not met, skip this field entirely.
-
Prompt if needed: If the field is required (
validations.required: true
) and no value has been determined, prompt the user. Use the field's
as the question and
/
as guidance.
-
Apply gathering notes: Use
config.fieldDefaults[fieldId].gatheringNotes
for additional guidance on how to present or gather this field.
Gathering style:
- Be conversational — don't present a wall of questions
- Batch related questions together (e.g., ask for summary and description in one turn)
- For dropdowns, present the options from the template
- For fields with defaults, mention the default and ask if it's correct
- Skip optional fields that the user hasn't mentioned unless they're likely relevant
Step 4: Compose Issue
Title
Build the title by substituting placeholders in the title pattern:
- Use if present; otherwise use the pattern extracted from the template in Step 2
- Replace placeholder text with gathered field values using reasoning (e.g., → the value of the field,
[Short descriptive title]
→ the field value)
Body
Render the issue body following the structure from the fetched template:
- yml templates: Render each field as a section with the gathered value. Use for empty optional sections. Maintain the exact field order from the template.
- md templates: Reconstruct the markdown body with gathered values filled in. Check the appropriate checkboxes, fill in labeled fields, and include all sections.
Labels
Build the label set:
- Start with
- Merge in template-level labels (from the fetched template's field) — avoid duplicates
- Apply
config.labels.conditional
rules:
- : Check if any keyword appears in the user's request (case-insensitive)
- : Check if the specified field has the specified value
- : Derive a label from a field value using the specified transform (e.g., converts "Document Status" to "document-status")
- Apply any additional label logic defined in the template config's
Assignees
Merge
with template-level assignees. If
config.assignees.promptUser
is true, ask the user if they want to assign anyone else.
Step 5: Preview & Confirm
Present the composed issue to the user for review:
**Title**: [composed title]
**Labels**: label1, label2, label3
**Assignees**: @user1, @user2
**Project**: [project board name] → [status]
**Body**:
[rendered body preview]
Ask for confirmation or edits. If the user requests changes, apply them and re-preview.
Step 6: Create Issue
Create the issue using the detected GitHub method:
method: create
owner: <config.repository.owner>
repo: <config.repository.repo>
title: <composed title>
body: <composed body>
labels: <label array>
assignees: <assignee array>
bash
gh issue create \
--repo <config.repository.owner>/<config.repository.repo> \
--title "<composed title>" \
--body "<composed body>" \
--label "<label1>" --label "<label2>" \
--assignee "<assignee1>" --assignee "<assignee2>"
- Pass each label and assignee as a separate / flag
- Use a heredoc for the body if it contains special characters:
bash
gh issue create \
--repo owner/repo \
--title "Title" \
--body "$(cat <<'EOF'
<composed body>
EOF
)" \
--label "label1" --label "label2"
- Parse the issue URL from the command output (printed to stdout on success)
Step 7: Post-Creation
Extract the issue URL from the creation response:
- If MCP: Get the URL from the response payload (e.g., field)
- If CLI: The command prints the issue URL to stdout
Always display the issue URL to the user as a clickable link, regardless of whether
is configured. This is the minimum required output on success.
If
config.postCreation.displayFormat
is defined, also render it by substituting
and
with actual values.
Display each item from
config.postCreation.additionalNotes
as a follow-up note.
Link Formatting Rules
When rendering links in the issue body, apply the rules from
config.linkFormatting.rules
in order. For each link:
- Check each rule's description to determine if it applies
- Apply the specified by the matching rule
- Use as link text if provided
Common patterns:
- GitHub issue/PR URLs as list items → raw URL (no markdown wrapping) so GitHub renders title + status
- Design links → markdown link with as text
- All other links → standard markdown
Acceptance Criteria
When
config.acceptanceCriteria
is defined:
- Start with
config.acceptanceCriteria.defaultItems
as baseline criteria
- Ask the user for additional criteria
- Render using
config.acceptanceCriteria.formatting.style
( = , = )
- Avoid the prefixes listed in
config.acceptanceCriteria.formatting.avoidPrefixes
Error Handling
Malformed settings.json
If
exists but fails validation against
references/settings-schema.json
:
- Notify the user that the settings file is invalid
- Show the specific validation error
- Offer to re-run the Setup Flow to create a new
GitHub config repo access failure
If the configured GitHub repo (for
configStorage.type === "github"
) is inaccessible:
- Notify the user that the config repository could not be reached
- Suggest checking: repository existence, access permissions, branch name
- If using CLI, suggest to verify authentication
- Offer to switch to local storage mode
Empty config directory
If the config directory (local or GitHub) exists but contains no
config files:
- Notify the user that no template configs were found
- Offer to create a first template config
Config write failure (GitHub)
If writing a config to GitHub fails:
- Notify the user of the failure
- Provide context about potential causes: branch protection rules, insufficient permissions, file conflicts
- If the error includes a SHA mismatch, suggest re-fetching the file and retrying
- Offer to save the config locally as a fallback
Template fetch failure
If the template fetch fails (MCP
or
):
- Notify the user that the template could not be fetched
- Suggest checking repository access permissions
- If using CLI, suggest running to verify authentication
- Offer to create the issue manually without template structure
JSON config parse failure
If a template config file is malformed:
- Skip that template during selection
- Notify the user which config failed to parse
Issue creation failure
If issue creation fails (MCP
or
):
- Stop the operation immediately
- Notify the user of the failure
- Provide context about potential causes: authentication token issues, permissions, rate limits, invalid repository access
- If using CLI, include the stderr output from the command for diagnostics
- Offer to display the composed issue body so the user can create it manually
Adding a New Template
To add support for a new issue type, create a new
config file following the schema in
. The save location depends on the storage mode configured in
:
Local storage (configStorage.type === "local"
)
- Create a new file in
~/.claude/configs/github-issue-from-templates/
- Follow the schema defined in
- Set and to the target GitHub repository
- Set to the repo-relative path of the GitHub issue template
- Set to or based on the template type
- Define for automatic template matching
- Add any , , label rules, and formatting overrides
- No changes to this SKILL.md file are needed
GitHub storage (configStorage.type === "github"
)
-
Compose the config JSON following
-
Commit the file to the configured repo:
owner: <configStorage.owner>
repo: <configStorage.repo>
path: <configStorage.path>/<filename>.json
content: <base64-encoded JSON>
message: "Add <template-name> template config"
branch: <configStorage.branch>
If CLI: Use the GitHub contents API:
bash
gh api repos/<owner>/<repo>/contents/<path>/<filename>.json \
--method PUT \
--field message="Add <template-name> template config" \
--field branch=<branch> \
--field content=$(echo '<JSON content>' | base64)
-
No changes to this SKILL.md file are needed
Updating an Existing Template Config
Local storage
Read, edit, and overwrite the
file in
~/.claude/configs/github-issue-from-templates/
directly.
GitHub storage
Updating a file via the GitHub contents API requires the current file's SHA. Follow these steps:
-
Fetch the current file to get its SHA:
If MCP: Use
— the response includes the
field.
If CLI:
bash
gh api repos/<owner>/<repo>/contents/<path>/<filename>.json?ref=<branch> --jq '.sha'
-
Update the file with the SHA included:
If MCP: Use
with the
parameter:
owner: <configStorage.owner>
repo: <configStorage.repo>
path: <configStorage.path>/<filename>.json
content: <base64-encoded updated JSON>
message: "Update <template-name> template config"
branch: <configStorage.branch>
sha: <current SHA>
If CLI:
bash
gh api repos/<owner>/<repo>/contents/<path>/<filename>.json \
--method PUT \
--field message="Update <template-name> template config" \
--field branch=<branch> \
--field content=$(echo '<updated JSON>' | base64) \
--field sha=<current SHA>
Section Formatting Conventions
- Use for any section where no information was provided — never omit sections
- Include all template sections in the exact order they appear in the fetched template
- Use bullet points for lists
- Use proper markdown formatting
- Label names: lowercase with hyphens (e.g., , )