---
sidebar_position: 6
question: Can I declare a reusable SPARQL query in Turtle?
kind: task
via_web: na
via_cli: na
via_python: available
via_api: available
via_config: na
ref_python: triple_store.query
prereq: For a reusable query, an in-engine module with an `ontologies/` folder. [Create a module](/user-guide/build/modules/creating-a-module) first.
next:
  - /capabilities/build/agents/give-it-tools
  - /capabilities/build/knowledge-graph/explore-the-graph
---

# Query with SPARQL

Code runs SPARQL on the triple store with `triple_store.query`. A query you want agents to reuse goes in a Turtle file of the module instead: the engine turns each one into a tool with validated arguments, with no Python workflow to write.

## Web UI

Not applicable. Nexus has no SPARQL editor: the Composer shows the SPARQL it ran, read-only ([Explore the graph](/user-guide/build/knowledge-graph/explore-the-graph?via=web)). On the Docker stack, YASGUI at `http://localhost:3000` edits SPARQL on Fuseki.

## CLI

Not applicable. There is no `abi` command that runs SPARQL. Send the query to the store's endpoint (REST API tab), or run it from Python.

## Python

### Run a query

The module lists `TripleStoreService` in its dependencies: see [Add data](/user-guide/build/knowledge-graph/add-data?via=python).

```python
store = self.engine.services.triple_store
rows = store.query("""
    PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
    SELECT ?s ?label
    WHERE { GRAPH <http://ontology.naas.ai/graph/my_module> { ?s rdfs:label ?label } }
    LIMIT 10
""")
for row in rows:
    print(row.s, row.label)
```

`query` returns an `rdflib` result. `query_stream` is a context manager for large results.

### Declare a reusable query

Add a Turtle file under the module's `ontologies/` folder. `abi new module <name> --in-engine` already writes `ontologies/queries/<Name>SparqlQueries.ttl` with two examples.

```turtle title="ontologies/queries/MyModuleSparqlQueries.ttl"
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix intentMapping: <http://ontology.naas.ai/intentMapping/> .

intentMapping:findMyModuleRecordByIdQuery a intentMapping:TemplatableSparqlQuery ;
    rdfs:label "find_my_module_record_by_id"@en ;
    intentMapping:intentDescription "Look up a single record by its identifier." ;
    intentMapping:sparqlTemplate """
        PREFIX my_module: <http://ontology.naas.ai/my_module/>
        SELECT DISTINCT ?record ?recordId
        WHERE {
          GRAPH <http://ontology.naas.ai/graph/my_module> {
            ?record my_module:record_id ?recordId .
            FILTER(STR(?recordId) = "{{ record_id }}")
          }
        }
    """ ;
    intentMapping:hasArgument intentMapping:myModuleRecordIdArg .

intentMapping:myModuleRecordIdArg a intentMapping:QueryArgument ;
    intentMapping:argumentName "record_id" ;
    intentMapping:argumentDescription "Identifier of the record in the source system." ;
    intentMapping:validationPattern "^[A-Za-z0-9_-]{1,64}$" ;
    intentMapping:validationFormat "string" .
```

`rdfs:label` becomes the tool's name and `intentDescription` its description: the agent picks the tool from them, so write the description as the question a user would ask. Each `{{ placeholder }}` needs a `QueryArgument` linked with `hasArgument`.

### Load it

Restart the engine. It loads every `*.ttl` under each module's `ontologies/` into the schema graph, and the `naas_abi_core.modules.templatablesparqlquery` module, loaded by default, turns each query into a tool.

### Give the tools to an agent

Declare the module as a dependency, then ask it for tools by name:

```python title="src/my_module/__init__.py"
dependencies: ModuleDependencies = ModuleDependencies(
    modules=["naas_abi_core.modules.templatablesparqlquery"],
    services=[TripleStoreService],
)
```

```python title="src/my_module/agents/MyAgent.py"
from my_module import ABIModule

sparql_module = ABIModule.get_instance().engine.modules[
    "naas_abi_core.modules.templatablesparqlquery"
]
tools += sparql_module.get_tools(["find_my_module_record_by_id"])
```

`get_tools()` with no names, or an empty list, returns every templated query tool. Call it inside the agent's `New()`.

## REST API

The Nexus API and the engine API have no route that takes raw SPARQL. The triple store's own endpoint does, on the machine that runs it.

### Send the query

With `abi dev up`, the store is the `oxigraph` server; `abi dev ports` prints its port (7878 plus the checkout's offset). It has no auth:

```bash
curl -X POST "http://127.0.0.1:7878/query" \
  -H "Content-Type: application/sparql-query" \
  -H "Accept: application/sparql-results+json" \
  --data 'SELECT ?g (COUNT(*) AS ?n) WHERE { GRAPH ?g { ?s ?p ?o } } GROUP BY ?g'
```

### Or on the Docker stack

Fuseki answers SPARQL on `http://localhost:3030/ds/query` (dataset `ds`). Its admin user is `admin`, with the `FUSEKI_ADMIN_PASSWORD` from `.env`.

## Config

Not applicable. Queries are Turtle files in the module, not `config.yaml` entries. `config.yaml` only picks the store: see [Triple Store](/user-guide/get-started/configuration#triple-store).

## How templated queries load

When the engine starts, `TemplatableSparqlQueryLoader` reads every `intentMapping:TemplatableSparqlQuery` from the schema graph:

1. A query is loaded only when it has an `rdfs:label`, an `intentDescription`, a `sparqlTemplate` and at least one `hasArgument`.
2. Each argument needs all four of `argumentName`, `argumentDescription`, `validationPattern` and `validationFormat`. A query with an incomplete argument is skipped, with a warning in the log.
3. Each argument becomes a required string field, checked against its `validationPattern` before the value goes into the template. Keep the pattern tight enough to block injection.
4. The query is wrapped in a `GenericWorkflow` that renders the template with Jinja2, runs it on the triple store and returns the rows. A value that does not match its `validationPattern` is refused before that, with a pydantic `ValidationError`. A template or query failure comes back as a row, `[{"error": "..."}]`, not as an exception.
5. Each workflow is exposed as a LangChain tool.

| Property | On | Meaning |
|---|---|---|
| `rdfs:label` | Query | Tool name |
| `intentMapping:intentDescription` | Query | Tool description, read by the agent |
| `intentMapping:sparqlTemplate` | Query | SPARQL with Jinja2 placeholders |
| `intentMapping:hasArgument` | Query | Links an argument; repeat for several |
| `intentMapping:argumentName` | Argument | Placeholder name in the template |
| `intentMapping:argumentDescription` | Argument | Shown to the agent |
| `intentMapping:validationPattern` | Argument | Regex the value must match |
| `intentMapping:validationFormat` | Argument | Human-readable format |

## Jinja2 templates

`sparqlTemplate` takes full Jinja2:

```turtle
intentMapping:sparqlTemplate """
    SELECT ?item ?name
    WHERE {
      ?item a :Product ; :hasName ?name .
      {% if min_price %}
      ?item :hasPrice ?price .
      FILTER(?price >= {{ min_price }})
      {% endif %}
    }
""" ;
intentMapping:hasArgument intentMapping:minPriceArg .
```

Every argument is required, so an optional clause needs a `validationPattern` that accepts an empty string (for example `^[0-9]*$`); the agent passes an empty value to skip it. Several arguments are one `hasArgument` with a list: `intentMapping:hasArgument intentMapping:arg1, intentMapping:arg2 .`

## Best practices

- **Names.** Use the scaffold's style: a snake_case label that reads as an action (`find_my_module_record_by_id`), and an `...Arg` suffix on argument resources.
- **Descriptions.** Say which question the query answers and what it leaves out. The agent chooses on that text.
- **Validation.** Give every argument the tightest pattern its values allow, and a clear `validationFormat`.
- **Query shape.** Declare every `PREFIX`, scope the pattern with `GRAPH <...>`, use `DISTINCT` when joins can repeat rows, order the results, and add a `LIMIT` for large sets.
