How to create a skill that works: description, SKILL.md and references
Creating a skill is easy; getting it to trigger at the right time is not. How to write the description, the router pattern, scripts and SKILL.md mistakes.

These two descriptions could belong to the same skill:
# Version A (made-up example)
description: Helps with Illuminate SQL queries.# Version B (the real description of the illuminate-sql-support skill)
description: Create, validate, explain, debug, and improve Snowflake SQL for
Blackboard Data / Illuminate using the packaged current schema, documented
fields, relationship candidates, examples, and domain notes. Use for
Illuminate reporting questions and SQL involving the supported CDM_ALY,
CDM_CLB, CDM_LMS, CDM_MAP, CDM_MEDIA, CDM_META, or CDM_TLM schemas.A is what almost anyone writes the first time, and there is nothing wrong with it. What it does not give the agent is a reason to load the skill when someone pastes a query with CDM_LMS.COURSE_ACTIVITY and asks why it returns twice as many rows, which is exactly when the skill has something to contribute. B says which tasks it covers (create, validate, explain, debug, improve), what material it works with and which schemas it applies to, by their exact names.
This article walks through the anatomy of a skill starting from that difference, using illuminate-sql-support as the common thread. It is a skill we have built at eLearning Media that generates and reviews Snowflake SQL for Blackboard Data / Illuminate, the analytics data warehouse of that LMS. I rebuild it as an evolving example: the intermediate stages are illustrative and the final excerpts are taken verbatim from the skill.
It cannot be installed yet, because ChatGPT does not accept new skills at the moment. When it does, or once the skill is published in a repository, we will add install instructions to Consultas SQL para Blackboard (in Spanish).
If the difference between a skill and an MCP server is still fuzzy, start with MCP vs Skills; for the full map of the topic, see the guide to agent skills in a development team.
What the agent sees before deciding
A skill is a folder with a SKILL.md and, optionally, more files. The SKILL.md starts with YAML frontmatter containing two required fields, name and description, followed by Markdown instructions. That is where most tutorials stop.
What actually shapes how you should write it is the way it gets loaded. The agent does not read every available skill in every conversation. At startup, it only has the name and description of each one in context. When it decides one fits the task, it loads the body of the SKILL.md. And it only reads another file in the folder if that body tells it to open it. The open Agent Skills standard calls this progressive disclosure.
The design decisions in this article follow from that. The description is the only thing that exists for the agent until the skill is activated, so its job is to trigger activation at the right moment. The body of the SKILL.md is loaded in full every time the skill is used, so it should contain what applies to any task within its scope and little else. And supporting files cost almost nothing until they are read, so they can be huge as long as the body knows how to point to them.
In the Illuminate skill this shows in the proportions. The SKILL.md has 95 lines. The references add up to about 5,200: the full database structure (1,595 lines), a CSV of candidate relationships between tables (1,499), a flat index of valid fields (998), SQL examples (475) and domain notes on tool types and semi-structured data (588). Loading all of that for every question would be absurd; loading none of it would force the model to invent column names.
First version: everything in one file
Suppose someone decides to turn their Illuminate knowledge into a skill. The first version usually looks something like this (illustrative example):
---
name: illuminate
description: Helps with Illuminate SQL queries.
---
# Illuminate
You are an Illuminate expert. These are the available tables:
CDM_LMS.COURSE: ID, NAME, COURSE_NUMBER, ...
CDM_LMS.PERSON: ID, FIRST_NAME, LAST_NAME, ...
(hundreds of lines with tables, columns and a few example queries)It has two flaws that reinforce each other. The description competes with anything else that talks about SQL: another database skill, an MCP server connected to Snowflake, or the model’s general knowledge, which thinks it knows what an LMS data model looks like. Nothing in it says which schemas it covers or what kind of questions it answers. And if it finally does trigger, it drops hundreds of lines into context even when the question is just what the event date column is called.
The name matters too, though less. The standard requires it to match the folder name, and it should be specific: illuminate stops being useful as soon as a second skill about the same product appears with a different purpose. illuminate-sql-support says what it does.
Second version: a description written to trigger
The real description has two parts. The first sentence says what the skill does, listing verbs that correspond to different requests: writing a query from scratch, validating an existing one, explaining what it does, debugging an error, improving it. Each verb is a different way someone might ask for help, and a description that only said “generates SQL” would not trigger on “why does this query fail?”.
The second part, “Use for Illuminate reporting questions and SQL involving the supported … schemas”, narrows the scope with exact identifiers. Schema names (CDM_LMS, CDM_TLM…) are the best activation signal this skill has, because they appear literally in the SQL people paste into the chat. A concrete identifier is far more reliable than a topical description.
Comparing this description with the ones in my team’s internal plugin, the triggers fall into a handful of families: the words people use to ask for things, domain identifiers (schema, class or file names), error messages exactly as they appear, and the scope that separates the skill from its neighbors. Error messages carry particular weight in operational skills, because someone pasting a log does not describe the task, they paste the error.
The Illuminate description has a limitation worth pointing out. It is in English, like the rest of the skill, and the skill itself says to answer in the user’s language. Someone asking in Spanish “how many students accessed the course last week?”, without mentioning any schema, depends on the model connecting that question to “Illuminate reporting questions”. Adding a few Spanish phrasings to the description would reduce that dependency.
Third version: SKILL.md as a router
With activation sorted, the body remains. The pattern we use is to treat the SKILL.md as a router: a short file with the purpose, the limits, the workflow, the rules that cannot be broken and a section saying which file to open for each need. In the Illuminate skill that section is called “Route the references”:
## Route the references
Read references/reference-guide.md first whenever the request requires
schema-specific SQL. It explains source priority and how to use the
packaged references.
Then load only what the task needs:
- Search references/valid-fields.txt to confirm exact SCHEMA.TABLE.COLUMN
names quickly.
- Read the relevant table sections in references/database-structure.md for
data types, nullability, and table context.
- Search references/illuminate-relations.csv for join candidates, then verify
both sides against the schema and business meaning.
- Consult references/sql-examples.md for patterns and style only after
validating every referenced object against the current schema.
- Consult references/tool-assignment-types-stage.md for assignment/tool
categorization and documented STAGE JSON access patterns.Three details in this excerpt apply to any skill.
Each line starts with a different verb depending on the file type. “Search” for the field index and the CSV, which are queried by looking up an exact string; “Read the relevant table sections” for the database structure, which is read section by section; “Consult” for the examples and notes. It tells the agent how to use each file, not just that it exists.
There is a mandatory entry file. reference-guide.md is 45 lines long and explains the order of priority when sources disagree: the current database structure overrides the field index, the index overrides the relationships, and the relationships override the examples. That hierarchy settles conflicts the agent would run into sooner or later, because the sources of a real skill are never perfectly consistent.
Each reference carries its level of trust. The relationships CSV is presented as “candidates”, with an explicit warning in the guide: two tables sharing a column like PERSON_ID or COURSE_ID does not prove they can be joined. The SQL examples are used “for intent and style, not object validation”, because some are incomplete or predate the current schema. Without those labels, the agent would treat an old example with the same authority as the schema.
This pattern is not exclusive to large skills. In my team’s internal plugin we also use it in small skills; what changes is the length of the list.
What goes in SKILL.md and what goes in the references
The criterion is fairly simple. The SKILL.md holds what applies to any task in the skill and anything that breaks something if ignored. The references hold what is only needed for a specific task.
In Illuminate, the body contains the supported scope, the seven-step workflow, the SQL rules, the cost warning, a pre-check and the response format. Table and column names, relationships and examples live outside it. The line between the two is whether the agent needs the information to decide what to do or only to do it.
Fourth version: rules, limits and verification
With the router in place, the skill already loads what it needs. What makes it reliable is the rest of the body, and here the Illuminate skill has several decisions worth copying.
The scope is closed in both directions. It lists the seven supported schemas and names the ones it excludes: “Do not generate, suggest, or reconstruct queries against LEARN, CDM_CRM, or CDM_SIS”. If someone asks for something that is not in the packaged schema, the instruction is to say it falls outside the supported model instead of attempting it.
The rule against invention is explicit and concrete: “Never invent schemas, tables, fields, payload keys, relationships, or enum meanings”. And one step in the workflow reinforces it for the most tempting case: “Never infer undocumented LMS objects from general product knowledge”. A model knows a lot about LMSs in general, and that is precisely why it tends to fill gaps with plausible names that do not exist.
The limit of what the skill does is written at the start: it does not authenticate, does not connect to a tenant, does not run queries and cannot prove that a query works in a specific environment. A rule at the end closes it off: “Do not claim that SQL has been executed or validated against live tenant data”. Without that sentence, an answer saying “this query is correct” can sound like something that has been checked when it is only a well-written query.
There is a mandatory warning tied to a real risk. Queries on CDM_TLM, the telemetry data, can scan huge volumes and consume Illuminate credits. The skill requires a verbatim warning next to any query touching that schema and prioritizes filters on EVENT_TIME. When a rule has a financial cost, it is not left to the model’s judgment.
Verification happens before answering. The “Preflight before returning SQL” section asks the agent to silently check that every schema is supported, that every table and column exists, that tables are qualified, that each join has a documented path, that the grain of the result matches what was asked, that high-volume queries have filters or limits and that the cost warning is where it should be. It is a checklist the agent applies to its own output, and the word “silently” keeps the answer from filling up with narrated checks.
The response format is fixed: material assumptions, an SQL block and notes on parameters, joins, grain and risks. One last rule protects the conversation’s context: “Do not reproduce the packaged reference files wholesale”.
When a rule should be a script
The Illuminate skill has no scripts. All its material is data (Markdown, CSV and a TXT file) that the agent queries by searching. To validate that a column exists, searching for CDM_LMS.COURSE.COURSE_NUMBER in the flat index is enough, and an agent does that well.
The limit shows up when the check is long and mechanical. As an example, a script that extracted every SCHEMA.TABLE.COLUMN reference from a query and compared them against valid-fields.txt would do in milliseconds, without mistakes, what the preflight asks the model to do by reading. With a ten-line query it is not worth it; with a two-hundred-line one, it probably is.
The criteria I would apply for putting a script in a skill: the check is deterministic, the reference material is too large to read carefully on every use, and the result is useful outside the agent too (a person can run it, and so can a pipeline). When the check has to run every time, and not only when the agent remembers to call it, its place is no longer the skill but a hook; I cover that in the article on how to organize skills, commands and hooks in a plugin.
Sometimes the answer is neither a skill nor a script. We have had many skills that seemed like a good idea at first and in fact only added a layer of complexity to company processes that needed to be proceduralized or automated, not packaged for an agent. I go into that in when not to create a skill.
Mistakes when structuring a skill
Almost all of them follow from how loading works.
Duplicating content between the SKILL.md and the references. If a rule appears summarized in the body and expanded in a file, the two versions will end up saying different things. The Illuminate skill avoids this with the priority guide: when two sources disagree, it is written down which one wins.
Leaving orphan files. A reference the router does not mention is invisible: the agent does not browse the folder on its own. All five Illuminate references appear in “Route the references” and in the guide.
Not marking the boundaries with other skills or tools. Illuminate says what it does not do (connect, execute) and which schemas it does not cover. In a plugin with several skills, each one should also say what the neighboring skill covers, so the agent does not try to solve with one what is documented in another.
Including credentials or internal data. A skill gets loaded into a model’s context and is distributed with the plugin; treat it as code that could end up in any repository. If you install third-party skills, the risk runs the other way, and I cover it in the article on third-party skill security.
Writing for the person instead of the agent. A skill needs no introduction or historical background; it needs the order of the steps, the rules, the limits and what to do when something cannot be verified. Which knowledge deserves to be there I discussed in skills as living documentation. Where the knowledge in this skill came from (an earlier Custom GPT) and how it is distilled is in the article on distilling team knowledge.
How far a good description goes
All of the above increases the odds that the skill triggers when it should and that, once active, the agent reads what it needs and does not invent what it cannot find. It does not guarantee it. Activation is decided by the model with the information it has in each conversation, and it can change between model versions without anyone touching the skill. Even the Illuminate preflight is an instruction, not an executed check: the model can skip it. The only way to know whether a skill gets it right is to test it with requests that should trigger it and others that should not, and compare its answers with and without it. That is the subject of skill evals: without them, any improvement to the description is an assumption nobody has verified.


