| name | sparql-generation |
| description | SPARQL generation skill that constructs a SPARQL query from resolved Wikidata entities, properties, and graph paths. Use when you have verified Wikidata IDs and need to assemble them into a syntactically correct SPARQL query with proper triple patterns, filters, and aggregations. |
| compatibility | Requires Python 3, uv. No external API access needed for generation; validation requires requests for optional endpoint check. |
SPARQL Generation
Use this skill to generate SPARQL queries from resolved Wikidata entities, properties, and verified graph paths. The skill constructs syntactically correct queries using proper Wikidata modeling patterns.
Files
scripts/sparql_generator.py: standalone script for generating and validating SPARQL queries
references/sparql-1.1-query-features.md: SPARQL 1.1 language reference for query forms, property paths, aggregation, subqueries, VALUES, BIND, negation, and solution modifiers
references/wikidata-data-model.md: Wikidata RDF/modeling reference for wdt: vs p:/ps:, qualifiers, references, ranks, normalized values, datatype handling, and statement/value nodes
When To Use This Skill
Use this skill when:
- You have resolved entity IDs (QIDs) and property IDs (PIDs) from wikidata-search
- You have verified graph paths from graph-exploration
- You need to assemble a SPARQL query with proper triple patterns
- You want to validate SPARQL syntax before execution
- You need to apply common Wikidata query patterns (subclass traversal, label service, qualifiers)
Reference Usage
Consult the bundled references whenever the query requires details beyond the core patterns in this file:
- Use
references/sparql-1.1-query-features.md when deciding which SPARQL construct to use or how to structure it correctly. Typical cases: OPTIONAL vs UNION, FILTER NOT EXISTS vs MINUS, property-path syntax, VALUES, BIND, aggregates with GROUP BY/HAVING, subqueries, ORDER BY with LIMIT, and variable-scope questions.
- Use
references/wikidata-data-model.md when deciding which Wikidata RDF layer to query. Typical cases: choosing wdt: vs p:/ps:, accessing qualifiers with pq:, references with pr:, ranks with wikibase:rank, normalized quantities via psn:/wikibase:quantityAmount, datatype-specific handling, and understanding truthy versus full statement semantics.
- Prefer this
SKILL.md for task-specific generation rules and project-specific guardrails; use the reference documents to resolve syntax/modeling uncertainty, not to replace the workflow here.
Requirements
Install skill dependencies from the workspace root with uv sync.
The script uses rdflib for SPARQL parsing/validation (optional) and basic Python for query construction. No API keys required.
Environment Variables
None required.
Safety Rules
- Generate only read-only queries (SELECT, ASK, CONSTRUCT, DESCRIBE).
- Always include a LIMIT clause during iterative development (remove only for final verified queries if appropriate).
- Never generate DELETE, INSERT, or UPDATE operations.
- Use verified IDs only — never guess QIDs or PIDs.
Script Modes
1. Validate SPARQL Syntax (validate)
Check that a SPARQL query is syntactically valid:
uv run python .agents/skills/sparql-generation/scripts/sparql_generator.py \
--mode validate \
--sparql "SELECT ?x WHERE { ?x wdt:P31 wd:Q5 . } LIMIT 10"
2. Generate from Template (generate)
Generate a SPARQL query from structured inputs:
uv run python .agents/skills/sparql-generation/scripts/sparql_generator.py \
--mode generate \
--question "What is the capital of France?" \
--entities '{"France": "Q142"}' \
--properties '{"capital": "P36"}' \
--pattern "direct-lookup"
3. Apply Pattern (pattern)
Apply a named query pattern with entity/property substitution:
uv run python .agents/skills/sparql-generation/scripts/sparql_generator.py \
--mode pattern \
--pattern "type-filter" \
--entities '{"type": "Q6256", "constraint_property": "P30", "constraint_value": "Q18"}' \
--properties '{"output_property": "P36"}'
Script Usage
Arguments
Required:
--mode / -m: Operation mode (validate, generate, pattern)
Mode-specific:
--sparql: SPARQL query string (required for validate)
--question: Natural-language question (for generate mode context)
--entities: JSON object mapping entity names to QIDs
--properties: JSON object mapping property names to PIDs
--paths: JSON array of verified graph paths from exploration
--pattern: Named pattern to use (direct-lookup, reverse-lookup, type-filter, aggregation, qualifier, subclass, date-filter, top-k)
Optional:
--include-labels: Include the Wikidata label service (default: true)
--limit: Add a LIMIT clause (default: none for final, 20 for exploration)
--output-file: Write result JSON to a file instead of stdout
Return Shape
Validation Result
{
"success": true,
"mode": "validate",
"sparql": "SELECT ?x WHERE { ?x wdt:P31 wd:Q5 . } LIMIT 10",
"valid": true,
"errors": [],
"warnings": ["No label service included — results will show URIs instead of labels"],
"error": null
}
Generation Result
{
"success": true,
"mode": "generate",
"question": "What is the capital of France?",
"sparql": "SELECT ?capital ?capitalLabel WHERE {\n wd:Q142 wdt:P36 ?capital .\n OPTIONAL { ?capital rdfs:label ?capitalLabel FILTER(LANG(?capitalLabel) = \"en\") }\n}",
"assumptions": [
"France = Q142 (country)",
"capital = P36 (capital property)",
"Direct property lookup (wdt:) — no qualifiers needed"
],
"pattern_used": "direct-lookup",
"error": null
}
Query Patterns Reference
1. Direct Property Lookup
# "What is the X of Y?"
SELECT ?value ?valueLabel WHERE {
wd:Q_ENTITY wdt:P_PROPERTY ?value .
OPTIONAL { ?value rdfs:label ?valueLabel FILTER(LANG(?valueLabel) = "en") }
}
2. Reverse Lookup
# "What entities have property X pointing to Y?"
SELECT ?entity ?entityLabel WHERE {
?entity wdt:P_PROPERTY wd:Q_TARGET .
OPTIONAL { ?entity rdfs:label ?entityLabel FILTER(LANG(?entityLabel) = "en") }
}
3. Type Filter
# "Which entities of type T satisfy condition C?"
SELECT ?entity ?entityLabel WHERE {
?entity wdt:P31 wd:Q_TYPE ;
wdt:P_FILTER ?filterValue .
FILTER(?filterValue > threshold)
OPTIONAL { ?entity rdfs:label ?entityLabel FILTER(LANG(?entityLabel) = "en") }
}
4. Subclass Traversal
# "All entities that are instances of T or any subclass of T"
SELECT ?entity ?entityLabel WHERE {
?entity wdt:P31/wdt:P279* wd:Q_TYPE .
OPTIONAL { ?entity rdfs:label ?entityLabel FILTER(LANG(?entityLabel) = "en") }
}
5. Aggregation (COUNT, AVG, etc.)
# "How many entities of type T?"
SELECT (COUNT(?entity) AS ?count) WHERE {
?entity wdt:P31 wd:Q_TYPE .
}
6. Top-K / Ordering
# "Top N entities by property value"
SELECT ?entity ?entityLabel ?value WHERE {
?entity wdt:P31 wd:Q_TYPE ;
wdt:P_MEASURE ?value .
OPTIONAL { ?entity rdfs:label ?entityLabel FILTER(LANG(?entityLabel) = "en") }
}
ORDER BY DESC(?value)
LIMIT N
7. Date Filters
# "Entities where date property is after/before a date"
SELECT ?entity ?entityLabel ?date WHERE {
?entity wdt:P31 wd:Q_TYPE ;
wdt:P_DATE ?date .
FILTER(?date >= "2000-01-01T00:00:00Z"^^xsd:dateTime)
OPTIONAL { ?entity rdfs:label ?entityLabel FILTER(LANG(?entityLabel) = "en") }
}
Important: Always use the full ISO 8601 format with time component (T00:00:00Z) for xsd:dateTime comparisons. Bare date strings (e.g., "2000-01-01"^^xsd:dateTime) fail on some endpoints.
8. Qualifier Access
# "What is the value of property P with qualifier Q?"
SELECT ?value ?valueLabel ?qualifier WHERE {
wd:Q_ENTITY p:P_PROPERTY ?stmt .
?stmt ps:P_PROPERTY ?value ;
pq:P_QUALIFIER ?qualifier .
OPTIONAL { ?value rdfs:label ?valueLabel FILTER(LANG(?valueLabel) = "en") }
}
9. OPTIONAL for Non-Required Fields
# "List entities with property X, include Y if available"
SELECT ?entity ?entityLabel ?x ?y WHERE {
?entity wdt:P31 wd:Q_TYPE ;
wdt:P_X ?x .
OPTIONAL { ?entity wdt:P_Y ?y . }
OPTIONAL { ?entity rdfs:label ?entityLabel FILTER(LANG(?entityLabel) = "en") }
}
10. VALUES for Known Entity Sets
# "Information about specific entities"
SELECT ?entity ?entityLabel ?value WHERE {
VALUES ?entity { wd:Q1 wd:Q2 wd:Q3 }
?entity wdt:P_PROPERTY ?value .
OPTIONAL { ?entity rdfs:label ?entityLabel FILTER(LANG(?entityLabel) = "en") }
}
Generation Rules
Before finalizing a query, cross-check any non-trivial syntax against references/sparql-1.1-query-features.md and any statement-level or datatype-specific Wikidata modeling against references/wikidata-data-model.md.
-
Use only verified IDs: Never guess QIDs or PIDs. All IDs must come from wikidata-search or graph-exploration.
-
Correct triple pattern direction: Verified in graph exploration. wd:Q wdt:P ?obj vs ?subj wdt:P wd:Q.
-
Include labels using rdfs:label: Use OPTIONAL { ?x rdfs:label ?xLabel FILTER(LANG(?xLabel) = "en") } for human-readable labels. NOTE: SERVICE wikibase:label is NOT available on the custom endpoint — it causes HTTP 500 errors. Always use rdfs:label OPTIONAL pattern instead. Omit labels entirely for count/aggregate-only queries.
-
Use VALUES for known entities from the question: When the question explicitly names a small set of entities (e.g., "information about France, Germany, and Italy"), use VALUES ?entity { wd:Q1 wd:Q2 wd:Q3 }. However, NEVER use VALUES to inject entities discovered from world knowledge that the endpoint didn't return — if a declarative query returns fewer results than expected, those ARE the results.
-
Use OPTIONAL sparingly: Only for genuinely optional fields that may not exist on all entities.
-
Avoid Cartesian products: Ensure all triple patterns share variables or are properly constrained.
-
Use DISTINCT when needed: Especially after JOINs that could produce duplicates.
-
Bound exploration queries: Always include LIMIT during iterative development.
-
Use p:/ps:/pq: for qualifiers: Switch from wdt: when qualifier access is needed.
-
Filter placement: Place FILTERs close to the triple patterns they constrain.
-
Use normalized values for quantities: When retrieving numeric quantity values (distance, mass, area, duration, speed, etc.), ALWAYS use the normalized value path p:P.../psn:P.../wikibase:quantityAmount instead of wdt:P.... This returns SI-normalized values (meters, kg, m², seconds) which are consistent regardless of how the data was entered. See "Quantity Property Patterns" below.
-
Use full statement model for temporal/historical data: When the question asks about historical extremes ("highest ever", "all-time record") or needs to count ALL statements including past ones (e.g., "how many spouses total"), use p:P.../ps:P... to access all statements. The prefix only returns the single "truthy" (current/best-ranked) value and misses historical data. However, for "current state" questions about (like capitals, members, affiliations), prefer truthy paths which already reflect Wikidata's rank-based currentness semantics. Only switch to statement-level access when you need to explicitly inspect ALL statements (including deprecated/historical ones) or when the truthy path demonstrably misses data that should be there.
Rules for Mention-Provided Entities
-
Rule: Verify class suitability — Before using a mention entity in wdt:P31 wd:Q_entity, run a quick count. If it returns 0 results, the entity may be a concept/movement rather than a type. Search for the correct classifying entity.
-
Rule: Check for combined/intermediate classes — When the question combines two concepts (e.g., "NP-complete video games"), prefer using a single intermediate class (wdt:P31 wd:Q_combined_class) over intersecting two separate type constraints. Intermediate classes are more complete in Wikidata.
-
Rule: Don't assume mention properties are the linking property — When a mention has a property field, it may indicate the property's role in the question semantics, not necessarily the exact triple pattern to use. Always verify through graph exploration.
Common Mistakes to Avoid
| Mistake | Fix |
|---|
Using wdt:P31 wd:Q5 for subclasses | Use wdt:P31/wdt:P279* wd:Q5 for subclass inclusion |
Using wdt:P106/wdt:P279* for occupations | Prefer direct wdt:P106 wd:Q_OCCUPATION first — subclass traversal on occupation over-generates. Only add /wdt:P279* if the direct query returns < 3 results for a broad category |
| Missing label service | Use OPTIONAL { ?x rdfs:label ?xLabel FILTER(LANG(?xLabel) = "en") } (not SERVICE wikibase:label which is unavailable) |
| Wrong property direction | Verify with graph exploration before generating |
Unbounded P279* traversal | Add LIMIT or restrict depth |
| Filtering on labels instead of IDs | Use wd:QID not FILTER(CONTAINS(?label, "...")) |
| Missing DISTINCT with multiple optional patterns | Add DISTINCT to SELECT |
Using FILTER(?x = wd:Q...) instead of direct triple | Use wd:Q... wdt:P ?y directly |
| Qualifying objects in SELECT without GROUP BY | Use aggregate functions or remove from SELECT |
Using wdt: for quantity values | Use p:P.../psn:P.../wikibase:quantityAmount for normalized SI values |
Using wdt: for historical maximums | Use p:P.../ps:P... to access all historical statements |
| Returning label strings for item-valued properties | Return the entity QID (e.g., ?givenName as QID, not its label) |
Using direct wdt:P131 for location queries | Use wdt:P131+ (transitive) to include sub-divisions |
| Adding plausibility filters not in the question | Do not add FILTER(?x < 3) or restrict to solar system unless asked |
| Comparing monetary values without unit filter | Use psv: + wikibase:quantityUnit to filter for same currency |
Quantity Property Patterns
When a question asks for a numeric measurement (distance, mass, area, duration, speed, height, etc.), always use the normalized value path. This ensures consistent SI units regardless of how the data was originally entered.
Pattern: Retrieve a normalized quantity value
# "How far is X from Earth?" / "What is the area of X?" / "How heavy is X?"
SELECT ?value WHERE {
wd:Q_ENTITY p:P_QUANTITY/psn:P_QUANTITY/wikibase:quantityAmount ?value .
}
Examples:
- Distance from Earth (P2583):
p:P2583/psn:P2583/wikibase:quantityAmount → meters
- Duration (P2047):
p:P2047/psn:P2047/wikibase:quantityAmount → seconds
- Mass (P2067):
p:P2067/psn:P2067/wikibase:quantityAmount → kilograms
- Area (P2046):
p:P2046/psn:P2046/wikibase:quantityAmount → square meters
- Height (P2048):
p:P2048/psn:P2048/wikibase:quantityAmount → meters
- Speed (P2052):
p:P2052/psn:P2052/wikibase:quantityAmount → meters per second
- Wheelbase (P3039):
p:P3039/psn:P3039/wikibase:quantityAmount → meters
- Course length (P3157):
p:P3157/psn:P3157/wikibase:quantityAmount → meters
Pattern: Top-K by quantity with normalization
# "What is the tallest/fastest/heaviest X?"
SELECT ?entity WHERE {
?entity wdt:P31/wdt:P279* wd:Q_TYPE .
?entity p:P_QUANTITY/psn:P_QUANTITY/wikibase:quantityAmount ?value .
}
ORDER BY DESC(?value)
LIMIT 1
Pattern: Monetary comparison with unit filter
# "Which X has the highest box office (in USD)?"
SELECT ?entity WHERE {
?entity wdt:P31/wdt:P279* wd:Q_TYPE .
?entity p:P2142 ?stmt .
?stmt psv:P2142 ?valueNode .
?valueNode wikibase:quantityAmount ?amount ;
wikibase:quantityUnit wd:Q4917 . # Q4917 = US dollar
}
ORDER BY DESC(?amount)
LIMIT 1
Temporal and Historical Patterns
Pattern: Historical maximum (all-time best)
# "What is the highest Elo rating ever?" — need ALL statements, not just current
SELECT (MAX(?value) AS ?max) WHERE {
?entity p:P1087/ps:P1087 ?value .
}
Pattern: Current position holder (most recent start date)
# "Who is the current PM of X?" — use position held with temporal ordering
SELECT ?person WHERE {
?person p:P39 ?stmt .
?stmt ps:P39 wd:Q_POSITION .
?stmt pq:P580 ?startDate .
}
ORDER BY DESC(?startDate)
LIMIT 1
Pattern: Current relationship (exclude ended)
# "Does X currently have a spouse?"
ASK WHERE {
wd:Q_PERSON p:P26 ?stmt .
?stmt ps:P26 ?spouse .
MINUS { ?stmt pq:P582 ?endDate }
}
Pattern: Summing complementary sub-properties
# "How many floors does building X have?" (total = above + below ground)
SELECT (?above + ?below AS ?totalFloors) WHERE {
wd:Q_BUILDING wdt:P1101 ?above ;
wdt:P1139 ?below .
}
Other examples of complementary properties that may need summing:
- P1101 (floors above ground) + P1139 (floors below ground) = total floors
- Multiple distance/length components when asking for "total"
Pattern: Ordinal qualifier access
# "What is the Nth item in sequence X?"
SELECT ?value WHERE {
wd:Q_ENTITY p:P_PROPERTY ?stmt .
?stmt pq:P1545 "N" . # series ordinal
?stmt ps:P_PROPERTY ?value .
}
Integration with text2sparql Pipeline
Input from Previous Steps
The generation step receives:
- From wikidata-search: Resolved QIDs and PIDs with labels
- From graph-exploration: Verified paths, directions, qualifier structures
Output to Next Step
The generated SPARQL is passed to sparql-execution for execution and validation.
Troubleshooting
- Syntax errors from validator: Check bracket matching, semicolons between triple patterns, and proper string escaping.
- Unsure which SPARQL feature to use: Check
references/sparql-1.1-query-features.md before improvising syntax for negation, aggregation, property paths, subqueries, or inline bindings.
- Unsure which Wikidata prefix/model to use: Check
references/wikidata-data-model.md before choosing between wdt:, p:/ps:, pq:, psv:, psn:, or rank/reference access.
- Wrong results direction: Swap subject/object positions and re-run.
- No results with subclass traversal: Try without
P279* first to isolate the issue.
- Label service not working: Do NOT use
SERVICE wikibase:label — it causes HTTP 500 errors on the custom endpoint. Use OPTIONAL { ?x rdfs:label ?xLabel FILTER(LANG(?xLabel) = "en") } instead.
- Timeout during generation: The generation script itself should be fast; if it's slow, it's likely a validation step querying the endpoint.