SQL Workflow Skill
0. Load Knowledge Base Context FIRST
Project-specific conventions, decisions, and quirks live in the Knowledge Base. Always consult before exploring the schema.
Step 0a — get_knowledge
Call once at the start of every task with a 1-line task_description. Returns the always-loaded baseline (org/project understanding + conventions) plus up to 5 task-relevant decisions/debugging/quirks. Treat the returned ## title blocks as authoritative for naming, joins, and known traps.
Step 0b — search_knowledge(query=...)
Call when you hit something unexpected — column meaning unclear, ambiguous join, surprising row count. Pass a 2–4 word query. It is a pure read — no side effects.
Step 0c — propose_knowledge
Call ONLY after you have completed work and verified a finding. Use it to record:
category="decisions" for choices made (auto-accepted).
category="debugging" for root-cause traps you hit and resolved (auto-accepted).
category="quirks" (scope=connection) for connector/dialect oddities (auto-accepted).
- Do NOT propose
understanding — humans only. Do NOT propose conventions or domain-rules as part of automated runs unless explicitly asked (these queue for human review).
- Title must be a slug (
^[a-z0-9-]+$, ≤120 chars). Body is markdown.
- On duplicate-title: re-call with
overwrite=true only if the prior doc is genuinely outdated.
What NOT to do
- Do not paste raw KB text back into SQL comments — reference the doc title instead.
- Do not call
propose_knowledge mid-exploration — only after success.
1. Schema Exploration — Do This First
Before writing any SQL, load KB context (Phase 0 above) then understand the data:
- Read local schema files first (if schema/ directory exists in workdir):
schema/DDL.csv — all CREATE TABLE statements (if it exists)
schema/{table_name}.json — column names, types, descriptions, sample values
Reading these files costs zero tool calls and gives you table structure + sample data.
Only call MCP tools for information not in the local files (e.g., row counts, live data exploration).
- Call
list_tables to get all schemas and tables — only if no local schema files exist or you need row counts.
- Call
describe_table on the tables that seem relevant to the question (only if JSON files lack detail)
- Call
explore_column on categorical columns to see distinct values (for filtering/grouping)
- Call
find_join_path if you need to join tables and the relationship is unclear
Stop exploring after 3-5 tool calls. Write SQL based on what you've found.
2. Output Shape Inference — Before Writing SQL
Read the task question carefully for cardinality clues:
- "for each X" → GROUP BY X, one output row per X
- "top N" / "top 5" → LIMIT N or QUALIFY RANK() <= N
- "total / sum / average" → single row aggregate
- "list all" → detail rows, no aggregation
- "how many" → COUNT, result is 1 row 1 column
Write a comment at the top of your SQL:
-- EXPECTED: <row count estimate> rows because <reason from question>
Critical checks:
- If the question asks for a single number, the result MUST be 1 row × 1 column
- If the question says "how many", verify the CSV has exactly 1 row with a COUNT value
- If "top N" appears in the question, verify the CSV has at most N rows
3. Iterative Query Building — Build Bottom-Up
Do NOT write a 50-line query and run it all at once:
- Write the innermost subquery or first CTE first
- Run it standalone with
query_database — verify row count and sample values
- Add the next CTE, verify again
- Continue until the full query is built
Example incremental pattern:
-- Step 1: verify source
SELECT COUNT(*) FROM orders WHERE status = 'completed';
-- Step 2: verify join partner cardinality
SELECT COUNT(*), COUNT(DISTINCT customer_id) FROM orders;
-- Step 3: build first CTE, verify
WITH order_totals AS (
SELECT customer_id, SUM(amount) AS total
FROM orders
GROUP BY customer_id
)
SELECT COUNT(*), COUNT(DISTINCT customer_id) FROM order_totals;
-- Step 4: add final aggregation
4. Execution and Structured Verification
mcp__signalpilot__query_database
connection_name="<task_connection_name>"
sql="SELECT ..."
After executing, run these checks IN ORDER before saving:
- Row count sanity: Does 0 rows make sense? Does 1M rows make sense for a "top 10" question?
- Column count: Does the result have the right number of columns for the question?
- NULL audit: For each key column — unexpected NULLs indicate wrong JOINs:
SELECT COUNT(*) - COUNT(col) AS nulls FROM (your_query) t
- Sample inspection: Look at 5 rows — are values in expected ranges? Do string columns have meaningful values (not join keys)?
- Fan-out check: If JOINing, compare
COUNT(*) vs COUNT(DISTINCT primary_key):SELECT COUNT(*) AS total_rows, COUNT(DISTINCT <pk>) AS unique_keys FROM (your_query) t;
If they differ, you have duplicate rows from a fan-out JOIN.
- Re-read the question: Does your output actually answer what was asked?
5. Error Recovery Protocol
6. Saving Output
Once you have the correct result:
Write final SQL to result.sql:
Write tool: path="result.sql", content="<your SQL query>"
Write the result as CSV to result.csv:
Write tool: path="result.csv", content="col1,col2,...\nval1,val2,..."
- Always include a header row with column names
- Use comma as delimiter
- Quote string values that contain commas or newlines
7. Turn Budget Management
- First 3 turns: Schema exploration only (
schema_overview, describe_table on 2-3 tables, explore_column on key categorical columns). STOP exploring.
- Turns 4 through (N-3): Write query iteratively — execute and verify each step.
- Last 3 turns: Finalize
result.sql and result.csv. If you have a working query, SAVE IT NOW — do not keep iterating.
If your query works and passes all verification checks, SAVE IMMEDIATELY — do not continue exploring "just in case".
8. Common Benchmark Traps
- Rounding: Do NOT round unless the question explicitly asks for rounded values. The evaluator uses tolerance-based comparison — full precision is always safer.
- Column naming: Match the question's phrasing exactly. If the question says "total revenue", name the column
total_revenue, not sum_revenue or revenue_total.
- CSV format: No trailing newline, no BOM, comma delimiter, double-quote strings containing commas.
- Empty result: If the correct answer is 0 or empty, write a CSV with just the header row (or header + "0").
- Date/time format in CSV: Use ISO 8601 (
YYYY-MM-DD) unless the question specifies otherwise.
- String case in CSV: Preserve the case from the database — do not uppercase/lowercase unless the question explicitly asks.
- Fan-out from JOINs: Always check
COUNT(*) vs COUNT(DISTINCT key) after every JOIN
- Wrong NULL handling: Use
IS NULL / IS NOT NULL, not = NULL
- Date format mismatch: Check the actual format stored in the column with
explore_column
- Case sensitivity: Use the correct case-insensitive function for your backend
- Interpretation errors: Before saving, re-read the original question. Verify:
- Filter conditions match domain values (check with explore_column if unsure)
- "Excluding X" means the right thing (NOT IN vs EXCEPT vs WHERE NOT)
- Metrics match domain definitions (e.g., "scored points" in F1 = points > 0, not just participated)
1---2name: sql-workflow3description: Use this skill before writing any SQL query. Covers: output shape inference (cardinality clues from the question), efficient schema exploration, iterative CTE-based query building, structured verification loop (row count, NULL audit, fan-out check, sample inspection), error recovery protocol, saving output to result.sql and result.csv, turn budget management, and common benchmark traps.4---56# SQL Workflow Skill78## 0. Load Knowledge Base Context FIRST910Project-specific conventions, decisions, and quirks live in the Knowledge Base. Always consult before exploring the schema.1112### Step 0a — `get_knowledge`1314Call once at the start of every task with a 1-line `task_description`. Returns the always-loaded baseline (org/project understanding + conventions) plus up to 5 task-relevant decisions/debugging/quirks. Treat the returned `## title` blocks as authoritative for naming, joins, and known traps.1516### Step 0b — `search_knowledge(query=...)`1718Call when you hit something unexpected — column meaning unclear, ambiguous join, surprising row count. Pass a 2–4 word query. It is a pure read — no side effects.1920### Step 0c — `propose_knowledge`2122Call ONLY after you have completed work and verified a finding. Use it to record:2324- `category="decisions"` for choices made (auto-accepted).25- `category="debugging"` for root-cause traps you hit and resolved (auto-accepted).26- `category="quirks"` (scope=connection) for connector/dialect oddities (auto-accepted).27- Do NOT propose `understanding` — humans only. Do NOT propose `conventions` or `domain-rules` as part of automated runs unless explicitly asked (these queue for human review).28- Title must be a slug (`^[a-z0-9-]+$`, ≤120 chars). Body is markdown.29- On duplicate-title: re-call with `overwrite=true` only if the prior doc is genuinely outdated.3031### What NOT to do3233- Do not paste raw KB text back into SQL comments — reference the doc title instead.34- Do not call `propose_knowledge` mid-exploration — only after success.3536## 1. Schema Exploration — Do This First3738Before writing any SQL, load KB context (Phase 0 above) then understand the data:39400. **Read local schema files first** (if schema/ directory exists in workdir):41 - `schema/DDL.csv` — all CREATE TABLE statements (if it exists)42 - `schema/{table_name}.json` — column names, types, descriptions, sample values43 Reading these files costs zero tool calls and gives you table structure + sample data.44 Only call MCP tools for information not in the local files (e.g., row counts, live data exploration).451. Call `list_tables` to get all schemas and tables — only if no local schema files exist or you need row counts.462. Call `describe_table` on the tables that seem relevant to the question (only if JSON files lack detail)473. Call `explore_column` on categorical columns to see distinct values (for filtering/grouping)484. Call `find_join_path` if you need to join tables and the relationship is unclear4950Stop exploring after 3-5 tool calls. Write SQL based on what you've found.5152## 2. Output Shape Inference — Before Writing SQL5354Read the task question carefully for cardinality clues:5556- "for each X" → GROUP BY X, one output row per X57- "top N" / "top 5" → LIMIT N or QUALIFY RANK() <= N58- "total / sum / average" → single row aggregate59- "list all" → detail rows, no aggregation60- "how many" → COUNT, result is 1 row 1 column6162Write a comment at the top of your SQL:63```sql64-- EXPECTED: <row count estimate> rows because <reason from question>65```6667Critical checks:68- If the question asks for a single number, the result MUST be 1 row × 1 column69- If the question says "how many", verify the CSV has exactly 1 row with a COUNT value70- If "top N" appears in the question, verify the CSV has at most N rows7172## 3. Iterative Query Building — Build Bottom-Up7374Do NOT write a 50-line query and run it all at once:75761. Write the innermost subquery or first CTE first772. Run it standalone with `query_database` — verify row count and sample values783. Add the next CTE, verify again794. Continue until the full query is built8081Example incremental pattern:82```sql83-- Step 1: verify source84SELECT COUNT(*) FROM orders WHERE status = 'completed';8586-- Step 2: verify join partner cardinality87SELECT COUNT(*), COUNT(DISTINCT customer_id) FROM orders;8889-- Step 3: build first CTE, verify90WITH order_totals AS (91 SELECT customer_id, SUM(amount) AS total92 FROM orders93 GROUP BY customer_id94)95SELECT COUNT(*), COUNT(DISTINCT customer_id) FROM order_totals;9697-- Step 4: add final aggregation98```99100## 4. Execution and Structured Verification101102```103mcp__signalpilot__query_database104 connection_name="<task_connection_name>"105 sql="SELECT ..."106```107108After executing, run these checks IN ORDER before saving:1091101. **Row count sanity**: Does 0 rows make sense? Does 1M rows make sense for a "top 10" question?1112. **Column count**: Does the result have the right number of columns for the question?1123. **NULL audit**: For each key column — unexpected NULLs indicate wrong JOINs:113 ```sql114 SELECT COUNT(*) - COUNT(col) AS nulls FROM (your_query) t115 ```1164. **Sample inspection**: Look at 5 rows — are values in expected ranges? Do string columns have meaningful values (not join keys)?1175. **Fan-out check**: If JOINing, compare `COUNT(*)` vs `COUNT(DISTINCT primary_key)`:118 ```sql119 SELECT COUNT(*) AS total_rows, COUNT(DISTINCT <pk>) AS unique_keys FROM (your_query) t;120 ```121 If they differ, you have duplicate rows from a fan-out JOIN.1226. **Re-read the question**: Does your output actually answer what was asked?123124## 5. Error Recovery Protocol125126- **Syntax error**: Use `validate_sql` before `query_database` to catch errors without burning a query turn127- **Wrong results**: Do NOT just re-run the same query. Diagnose: which JOIN is wrong? Which filter is too aggressive?128- **Zero rows**: Binary-search your WHERE conditions — remove them one at a time to find the culprit:129 ```sql130 SELECT COUNT(*) FROM table WHERE cond_1; -- still same? keep it131 SELECT COUNT(*) FROM table WHERE cond_1 AND cond_2; -- drops? cond_2 is culprit132 ```133- **Too many rows**: Check for fan-out (duplicate join keys) or missing GROUP BY134- **CTE debugging**: Use `debug_cte_query` to run each CTE independently and find which step breaks135136## 6. Saving Output137138Once you have the correct result:1391401. Write final SQL to `result.sql`:141 ```142 Write tool: path="result.sql", content="<your SQL query>"143 ```1441452. Write the result as CSV to `result.csv`:146 ```147 Write tool: path="result.csv", content="col1,col2,...\nval1,val2,..."148 ```149 - Always include a header row with column names150 - Use comma as delimiter151 - Quote string values that contain commas or newlines152153## 7. Turn Budget Management154155- **First 3 turns**: Schema exploration only (`schema_overview`, `describe_table` on 2-3 tables, `explore_column` on key categorical columns). STOP exploring.156- **Turns 4 through (N-3)**: Write query iteratively — execute and verify each step.157- **Last 3 turns**: Finalize `result.sql` and `result.csv`. If you have a working query, SAVE IT NOW — do not keep iterating.158159If your query works and passes all verification checks, SAVE IMMEDIATELY — do not continue exploring "just in case".160161## 8. Common Benchmark Traps162163- **Rounding**: Do NOT round unless the question explicitly asks for rounded values. The evaluator uses tolerance-based comparison — full precision is always safer.164- **Column naming**: Match the question's phrasing exactly. If the question says "total revenue", name the column `total_revenue`, not `sum_revenue` or `revenue_total`.165- **CSV format**: No trailing newline, no BOM, comma delimiter, double-quote strings containing commas.166- **Empty result**: If the correct answer is 0 or empty, write a CSV with just the header row (or header + "0").167- **Date/time format in CSV**: Use ISO 8601 (`YYYY-MM-DD`) unless the question specifies otherwise.168- **String case in CSV**: Preserve the case from the database — do not uppercase/lowercase unless the question explicitly asks.169- **Fan-out from JOINs**: Always check `COUNT(*) vs COUNT(DISTINCT key)` after every JOIN170- **Wrong NULL handling**: Use `IS NULL` / `IS NOT NULL`, not `= NULL`171- **Date format mismatch**: Check the actual format stored in the column with `explore_column`172- **Case sensitivity**: Use the correct case-insensitive function for your backend173- **Interpretation errors**: Before saving, re-read the original question. Verify:174 * Filter conditions match domain values (check with explore_column if unsure)175 * "Excluding X" means the right thing (NOT IN vs EXCEPT vs WHERE NOT)176 * Metrics match domain definitions (e.g., "scored points" in F1 = points > 0, not just participated)