skill.md & Custom Commands
Every TaskFlow developer runs the same prompts over and over: "write a test for this router", "summarize the PR diff", "check if the schema matches the model." Skills let you write each of those prompts once, store them in .gemini/skills/, and invoke them with a short slash command — with arguments, tab completion, and no retyping.
What You'll Learn
- Understand what a skill.mdA Markdown file in .gemini/skills/ that registers a reusable custom slash command. Contains optional YAML frontmatter (name, description, arguments) plus a prompt body with {{placeholder}} template variables. file is and why it replaces retyped prompts
- Read and write the two-part skill structure: YAML frontmatterThe --- delimited block at the top of a Markdown file. In skill files it declares name:, description:, and arguments: that appear in tab-completion hints. + prompt body
- Use template variables{{arg_name}} placeholders in a skill's prompt body. Replaced with actual values at invocation time, so one skill file can handle many different inputs. to pass file paths and branch names into skills
- Create project-level and global skills — and know which wins when names collide
- Build a complete TaskFlow skill library: test-generator, pr-summary, schema-validator, endpoint-docs, migration-helper
What Are Skills?
BEFORE: Picture a customer support agent who types the same 12-sentence escalation email 30 times a day. Each time they start fresh: today they forgot to include the ticket number, yesterday they pasted the wrong SLA clause, last Friday they sent the entire email in lowercase. Every instance is slightly different, and the differences compound into real customer complaints.
PAIN: The same drift happens when you retype Gemini prompts by hand. Your "write tests for this router" prompt was great on Monday. By Thursday it's shorter, missing the "include error cases" instruction, and forgetting to reference @sample-project/taskflow/schemas.py. The inconsistency means your test coverage is inconsistent too.
MAPPING: A skill.md is a saved snippet for AI prompts. Instead of typing the full prompt each time, you write it once in .gemini/skills/test-generator.md, give it a name, and from that moment /test-generator file=routers/tasks.py expands into the complete, annotated, always-consistent prompt — including the right file references, the error-case requirement, and the schema import reminder.
A skill.md is a Markdown file placed in .gemini/skills/ (project-level) or ~/.gemini/skills/ (global). It has two parts: optional YAML frontmatter between --- delimiters that declares name:, description:, and arguments:; and a prompt body in plain Markdown that is sent to Gemini verbatim when the skill is invoked. The name: field becomes the slash command. {{argument_name}} placeholders in the body are replaced with values you pass at call time.
On startup, Gemini CLI scans both skill directories and registers each file as a /skill-name command. Type / in the REPL to see all skills — yours and built-ins — with tab completion.
Teams that standardize their code-review prompts catch 3–5x more issues than teams who free-form it each session. Skills enforce that consistency across your entire team. Better still: project-level skills live in .gemini/skills/ which gets committed to git — a new teammate who clones TaskFlow gets every skill immediately, zero onboarding steps.
on disk
/skill args
{{placeholders}}
sent to Gemini
in terminal
The .gemini/skills/ Directory
Skills live in one of two places. Project-level skills go inside your repo and are tracked by git. Global skills are personal — they apply to every project you open.
When a project skill and a global skill share the same name:, the project-level skill always wins. This lets TaskFlow override your personal test-generator with a project-specific version that knows about schemas.py and the pytest-asyncio fixture setup — without touching your global toolkit.
Writing Your First Skill: test-generator
The most common TaskFlow prompt is "write pytest tests for this router." Instead of typing that prompt every day with slightly different wording, let's encode it once. The goal: run /test-generator file=routers/tasks.py and get a complete test file with auth fixtures, happy-path tests, and error cases.
Step 1 — Create the directory
# Navigate to the TaskFlow project root first
cd sample-project\taskflow
# Create the skills directory
New-Item -ItemType Directory -Force .gemini\skills
# Confirm it exists
Get-ChildItem .gemini\
.gemini\skills\ in the TaskFlow root. -Force means it won't error if the directory already exists — safe to run repeatedly.
# Navigate to the TaskFlow project root first
cd sample-project/taskflow
# Create the skills directory
mkdir -p .gemini/skills
# Confirm it exists
ls -la .gemini/
Step 2 — Write the skill file
---
name: test-generator
description: Generate pytest tests for a TaskFlow router file
arguments:
- name: file
description: Router file path relative to project root (e.g. routers/tasks.py)
required: true
---
You are writing pytest tests for the TaskFlow FastAPI application.
Target file: @{{file}}
Context files to read first:
- @sample-project/taskflow/schemas.py — Pydantic models for request/response
- @sample-project/taskflow/models.py — SQLAlchemy ORM models
- @sample-project/taskflow/auth.py — get_current_user dependency
Requirements:
1. Use pytest with httpx.AsyncClient (already in requirements.txt)
2. Include a `test_client` fixture that creates a real in-memory SQLite DB
3. Include a `auth_headers` fixture that registers + logs in a test user
4. Test every endpoint in the file:
- Happy path (201/200)
- Auth required (401 when no token)
- Not found (404 for missing IDs)
- Validation error (422 for bad input)
5. Use descriptive test names: test_create_task_returns_201_with_valid_data
6. Add a docstring to each test explaining WHAT it validates and WHY it matters
Output: a complete, runnable test file. No pseudocode. No placeholders.
/test-generator with one required argument called file. The body (everything after the second ---) is the actual prompt Gemini receives. @{{file}} is resolved at runtime: @ tells Gemini to read the file, {{file}} inserts the argument value.
schemas.py to know what TaskCreate looks like, and models.py to understand the SQLAlchemy relationships. Without them, it writes tests that import wrong field names.
@/home/user/project/routers/tasks.py in skill files — they break for every other developer. Use project-root-relative paths. Gemini CLI resolves @ references relative to the current working directory when Gemini was started.
Step 3 — Invoke it
# From sample-project\taskflow\
gemini
# Inside the REPL, type:
/test-generator file=routers/tasks.py
# From sample-project/taskflow/
gemini
# Inside the REPL, type:
/test-generator file=routers/tasks.py
What Just Happened?
Gemini CLI found .gemini/skills/test-generator.md, replaced {{file}} with routers/tasks.py, prepended @ to get @routers/tasks.py, read that file plus schemas.py, models.py, and auth.py, and sent the fully-assembled prompt to Gemini Pro. The result is a runnable test file that knows about TaskCreate, get_current_user, and the task_tags join table — all without you typing any of that context.
Writing a PR Summarizer Skill
After every feature branch, you need a PR description. Currently you're copying the diff, pasting it into a chat window, writing a vague "fix stuff" title. Let's automate a real description that lists changed endpoints, rationale, and test coverage notes.
---
name: pr-summary
description: Generate a structured PR description from a branch diff
arguments:
- name: branch
description: The feature branch name (source of the PR)
required: true
- name: base
description: The base branch to diff against
required: false
default: main
---
You are generating a GitHub Pull Request description for the TaskFlow project.
Context:
- Project: FastAPI + SQLite task management API
- Key files: @sample-project/taskflow/main.py, @sample-project/taskflow/routers/tasks.py
Diff target: branch `{{branch}}` against `{{base}}`
Run this shell command first to get the diff: `git diff {{base}}...{{branch}}`
Then produce a PR description with these exact sections:
## Summary
(2-3 bullet points: what changed and why)
## Changed Endpoints
(list each API endpoint modified, with before/after behavior if applicable)
## Models / Schema Changes
(any SQLAlchemy model or Pydantic schema changes — none if unchanged)
## Test Coverage
(list test files affected or created; note any coverage gaps)
## Breaking Changes
(none, or list with migration instructions)
Keep it factual. Do not invent changes that aren't in the diff.
base has a default: main — making it optional. When you call /pr-summary branch=feat/add-tag-filter without specifying a base, Gemini CLI substitutes main automatically.
git diff instruction here means Gemini fetches live diff data rather than working from stale context in its window.
What Just Happened?
Gemini ran git diff main...feat/add-due-date-filter in your shell, read the live diff alongside routers/tasks.py for context, then structured the output according to the template in the skill body. The base argument defaulted to main because you didn't specify it. The PR description reflects the actual changes — not a hallucinated summary.
Release Notes Skill
At release time, you need a CHANGELOG entry. This skill takes a version number and a previous git tag and produces CHANGELOG-style notes from the commit log.
---
name: release-notes
description: Generate CHANGELOG entries from git commits between two tags
arguments:
- name: version
description: The new version being released (e.g. v1.2.0)
required: true
- name: since_tag
description: The previous release tag to diff from (e.g. v1.1.0)
required: true
---
Generate CHANGELOG-style release notes for TaskFlow version {{version}}.
First, fetch the commit history since the last release:
`git log {{since_tag}}..HEAD --oneline --no-merges`
Then read @sample-project/taskflow/main.py to understand the project structure.
Output the notes in Keep a Changelog format:
## [{{version}}] - {{today}}
### Added
(new features, new endpoints, new query params)
### Changed
(modified behavior in existing endpoints)
### Fixed
(bug fixes — include the commit hash in parentheses)
### Removed
(deprecated features removed in this release)
Rules:
- Group commits by type (feat/fix/chore/docs prefix)
- Skip pure chore/ci/docs commits in the output
- Use present tense ("Add filter", not "Added filter")
- If a commit message is ambiguous, note it as "(needs clarification)"
version and since_tag are required — there's no safe default for either. The skill instructs Gemini to run git log to get real commit data, then formats it according to the industry-standard "Keep a Changelog" spec.
/release-notes version=v1.2.0 since_tag=v1.1.0
Gemini fetches the actual commit log between v1.1.0 and HEAD, categorizes each commit, and produces a complete CHANGELOG section. Edit it for accuracy, then paste into your CHANGELOG.md.
Skill Argument Placeholders
Placeholder syntax controls how arguments behave: whether they're required, what the fallback is, and how they're referenced in the body.
/test-generator file=routers/tasks.pySyntax Reference
arguments:
# Required — Gemini CLI will error if not provided
- name: file
description: Router file path relative to project root
required: true
# Optional with default — uses "main" if caller omits it
- name: base
description: Base branch for diff
required: false
default: main
# Optional with no default — omitting leaves {{base}} unreplaced
# (usually a bug — always provide a default for optional args)
- name: since_tag
description: Previous git tag
required: false
default:, the placeholder {{since_tag}} stays literally in the prompt body when the arg is omitted. Gemini will then see the string {{since_tag}} and either hallucinate a value or ask for clarification. Always pair required: false with a sensible default:.
Using Arguments in the Body
# Plain text substitution
Analyze the module: {{file}}
# → Analyze the module: routers/tasks.py
# File read via @ prefix
Read and analyze: @{{file}}
# → Gemini reads routers/tasks.py contents
# In shell commands
`git diff {{base}}...{{branch}}`
# → git diff main...feat/add-filter
# In headings
## Review for version {{version}}
# → ## Review for version v1.2.0
{{file}} inserts the string routers/tasks.py as text. @{{file}} instructs Gemini CLI to actually read the file and embed its contents in the context window. For router analysis skills, you almost always want @{{file}} so Gemini sees the code, not just the filename.
Sharing Skills Across Projects
Global Skills: Your Personal Toolkit
Global skills live in ~/.gemini/skills/ and are available in every project you open. Good candidates are general-purpose prompts that don't reference project-specific files:
explain.md— explain a code snippet in plain Englishcommit-msg.md— generate a conventional commit message from staged diffdocs.md— write inline docstrings for a functionrefactor.md— refactor for readability without changing behavior
Team Skills via a Shared Repo
The most scalable pattern is a skills/ repository your entire team shares:
- Create a
team-skillsgit repo with all your standard skill files - Each developer symlinks (or copies) the directory:
~/.gemini/skills/ -> ~/team-skills/ - When someone improves
security-review.md, everyone gets the update on next pull
Skill files are Markdown — they're committed to git. Never embed API keys, database URLs, or personal tokens in skill bodies. Instead, use a shell command in the skill body to read from environment variables: `echo $JIRA_TOKEN`. Gemini CLI runs that command at invocation time, not at commit time.
Building a TaskFlow Skill Library
Here are all five skills tailored to the TaskFlow codebase. Each one references real TaskFlow files so Gemini gets proper context every time.
schema-validator — Full Skill File
---
name: schema-validator
description: Check that all SQLAlchemy model fields have matching Pydantic schemas
arguments: []
---
Compare the SQLAlchemy models and Pydantic schemas in TaskFlow.
Read these files:
- @sample-project/taskflow/models.py
- @sample-project/taskflow/schemas.py
For each SQLAlchemy model (User, Task, Tag), list:
1. **Fields in the model but missing from ALL schemas** — these are likely
accidentally exposed or forgotten. Flag as WARNING.
2. **Fields in schemas but not in the model** — these are likely typos or
removed fields still in the schema. Flag as ERROR.
3. **Type mismatches** — e.g., model uses DateTime but schema uses str.
Flag as WARNING.
Output format per model:
### ModelName
- [OK] field_name: type matches
- [WARN] field_name: in model, missing from schema
- [ERROR] field_name: in schema, not in model
Finish with a summary: "X errors, Y warnings found."
endpoint-docs — Full Skill File
---
name: endpoint-docs
description: Generate a FastAPI docstring for a TaskFlow endpoint function
arguments:
- name: endpoint
description: The Python function name (e.g. list_tasks, create_task)
required: true
- name: file
description: Router file containing the endpoint
required: false
default: routers/tasks.py
---
Generate a complete FastAPI docstring for the `{{endpoint}}` function in:
@{{file}}
Also read:
- @sample-project/taskflow/schemas.py — for request/response types
- @sample-project/taskflow/auth.py — to document auth requirements
The docstring must include:
- One-sentence summary (imperative mood: "List all tasks for the current user")
- Parameters section (path params, query params, request body fields)
- Returns section (HTTP status code + response schema)
- Raises section (401, 403, 404, 422 as applicable)
- Example section with a realistic curl command
Format as a Google-style Python docstring. Output only the docstring, ready to paste.
migration-helper — Full Skill File
---
name: migration-helper
description: Draft an Alembic migration for a TaskFlow model change
arguments:
- name: change
description: Plain-English description of the model change
required: true
---
Generate an Alembic migration script for TaskFlow.
Change description: {{change}}
Read the current models first:
@sample-project/taskflow/models.py
Then produce a complete Alembic migration file with:
1. Correct `upgrade()` function using `op.add_column`, `op.drop_column`, etc.
2. Correct `downgrade()` function that reverses the upgrade
3. Realistic `revision`, `down_revision`, and `branch_labels` values
4. A comment explaining why this migration is needed
Constraints:
- Use SQLAlchemy column types from `sqlalchemy` (Integer, String, DateTime, Boolean)
- For nullable additions, use `nullable=True` and `server_default` if appropriate
- For index changes, use `op.create_index` / `op.drop_index`
- Do not import models directly — Alembic scripts use op.* only
Output only the Python migration file. No explanation text.
Lab: Build and Run Skills
Follow these steps to create the test-generator skill and run it against TaskFlow's routers/tasks.py. By the end you'll have a runnable test file and a feel for iterating on skill templates.
Start TaskFlow to confirm it runs
cd sample-project\taskflow
pip install -r requirements.txt
uvicorn main:app --reload
cd sample-project/taskflow
pip install -r requirements.txt
uvicorn main:app --reload
Expected: INFO: Application startup complete. — visit http://localhost:8000/health to confirm.
Create the skills directory and test-generator skill
New-Item -ItemType Directory -Force .gemini\skills
# Now create the file using your editor, or paste the content from section 3
mkdir -p .gemini/skills
# Now create the file using your editor, or paste the content from section 3
Open Gemini CLI and verify the skill appears
gemini
# At the > prompt, type just a forward slash:
/
# Expected output: your skill appears in the list
Built-in skills:
/chat Start a conversation
...
Project skills (.gemini/skills/):
/test-generator Generate pytest tests for a TaskFlow router file
Run the skill on routers/tasks.py
/test-generator file=routers/tasks.py
Gemini should produce a complete test file. Look for: test_client fixture, auth_headers fixture, tests for list_tasks, create_task, get_task, update_task, delete_task.
Save the output and run pytest
# Copy Gemini's output to a test file
# Then run:
pytest tests\test_tasks.py -v
pytest tests/test_tasks.py -v
Iterate on the skill template
If tests are missing error cases, open .gemini/skills/test-generator.md and add a line to the prompt: "Ensure every endpoint has a test for the case where no auth token is provided (should return 401)." Re-run the skill — the improvement applies instantly to every future invocation.
What Just Happened?
You built a reusable, team-shareable prompt template grounded in the real TaskFlow codebase. Every future test file generated from this skill will: read the actual router, understand the real schema shapes, include the right fixtures, and enforce the same quality bar — because the prompt is canonical, not retyped each time.
Knowledge Check
Five questions on skills. All based on the TaskFlow examples you just worked through.
1. You create ~/.gemini/skills/test-generator.md globally and also .gemini/skills/test-generator.md inside the TaskFlow project. When you run /test-generator from the TaskFlow directory, which skill runs?
~/.gemini/skills/test-generator.md.gemini/skills/test-generator.md2. In a skill file, what is the difference between {{file}} and @{{file}} in the prompt body?
@ is just a style convention{{file}} inserts the filename as plain text; @{{file}} tells Gemini CLI to read the file and embed its contents@{{file}} is only valid in the YAML frontmatter, not in the body@{{file}} runs the file as a shell script3. You want the pr-summary skill to default to main when no base branch is provided. Which frontmatter declaration is correct?
required: true, default: mainrequired: false, default: mainoptional: true, fallback: main4. The test-generator skill references @sample-project/taskflow/schemas.py in its prompt body. Why is this important for the TaskFlow use case specifically?
TaskCreate, TaskUpdate, and Tag schemas — without it, generated tests use hallucinated field names that cause ValidationError at runtime5. Which of the following is the safest way to include an API key needed by a skill in the skill's prompt — without committing the secret to git?
.gitignore/my-skill key=sk-abc123`echo $MY_API_KEY`GEMINI.md since that file is already excluded from GEMINI context