> For the complete documentation index, see [llms.txt](https://cantonindexer.infradao.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://cantonindexer.infradao.com/tutorial.md).

# Tutorial

You will find a template in your PQS, look at its records, adapt an example filter and run it in your terminal. [Tutorial: run the server](/server.md) then deploys it to a node and reads the documents with SQL. The filter used here follows `ValidatorLicense` contracts.

Before you start:

* `cinder` is installed ([Install](/install.md)).
* `./cinder.toml` points at your PQS:

  ```toml
  pqs_url = "postgres://user:password@pqs-host:5432/pqs"
  ```

Documents and records go to stdout. Logs go to stderr.

## How a filter works

A filter is a TOML file. Its `source` says which records it reads:

| `source`    | One record for                                 |
| ----------- | ---------------------------------------------- |
| `lifecycle` | each contract create and each contract archive |
| `exercises` | each choice exercised                          |

Under `lifecycle`, if the contract implements interfaces, PQS also holds one record per interface view, at the same position and with the same contract id. Their documents would overwrite one another, so a `where` names one of them with `.template_fqn` ([A template with several interfaces](/filter-format.md#a-template-with-several-interfaces)).

A [record](/filter-format.md#records) is one PQS row as JSON. [`where`](/filter-format.md#the-where-grammar) decides which records to keep. `[select]` turns each kept record into one [document](/filter-format.md#documents): a JSON object with your columns, after the reserved `_`-prefixed columns that identify the record. A lifecycle document is identified by its contract id, so the document of the archive replaces the document of the create (`_op: "upsert"`). An exercise document is added (`_op: "append"`).

`cinder preview` and `cinder run` try a filter against PQS and print to your terminal. They write nothing anywhere. The node, `canton-indexer`, keeps every filter deployed to it up to date and writes the documents to its store.

The steps below find a template, look at its records, write the filter and run it.

## 1. Find a template

```sh
cinder types --templates --package splice-amulet
```

`cinder types` lists what PQS's type registry holds, one block per package. A block starts with the package name and the versions PQS has seen, then one line per template, interface or choice, with its full name:

```
splice-amulet (0.1.15, 0.1.16, 0.1.17, 0.1.18, 0.1.19, 0.1.20, 0.1.21, 0.1.22)
  template   splice-amulet:Splice.Amulet:Amulet
  template   splice-amulet:Splice.Amulet:AppRewardCoupon
  …
  template   splice-amulet:Splice.ValidatorLicense:ValidatorLicense
  …

splice-amulet-name-service (0.1.16, 0.1.17, 0.1.18, 0.1.19, 0.1.20, 0.1.21, 0.1.22, 0.1.23)
  template   splice-amulet-name-service:Splice.Ans:AnsEntry
  …
```

`--package` keeps every package whose name contains the text, so `splice-amulet` also lists `splice-amulet-name-service`.

Leave out `--templates` to list interfaces and choices as well. Add `--json` for one JSON object per line, with an `aliases` list. Use the full name (the first alias) in a filter.

## 2. Look at the records

```sh
cinder preview --source lifecycle \
  --where '.template_fqn == "splice-amulet:Splice.ValidatorLicense:ValidatorLicense"' \
  --limit 1 --format json
```

`preview` prints the records your `where` keeps, exactly as a filter sees them. With `--format json` they come as one indented JSON array. Each record has fields such as `.contract_id`, `.template_fqn`, `.payload`, `.event_type`, `.offset` and `.node_id`. The full list is under Records in [Filter format](/filter-format.md#records). Read `.payload` to find the paths you want to keep.

By default `preview` reads every offset PQS holds and stops after 10 matches. `--limit N` changes that (`0` for no limit), and `--range S:E` reads only offsets `S` to `E`. Without `--format json` each record is one line of JSON.

## 3. Write the filter

Start from an example in the releases repository:

```sh
curl -fsSLO https://raw.githubusercontent.com/InfraDAO/canton-indexer-releases/v0.9.0/examples/validator-licenses.toml
```

Without its header comment, it reads:

```toml
name = "validator_licenses"
source = "lifecycle"
version = 1
where = '.template_fqn == "splice-amulet:Splice.ValidatorLicense:ValidatorLicense"'

[select]
validator = 'party(.payload.validator)'
sponsor = 'party(.payload.sponsor)'
dso = 'party(.payload.dso)'
version = '.payload.metadata.version'
contact_point = '.payload.metadata.contactPoint'
last_active_at = 'time(.payload.lastActiveAt)'
metadata_updated_at = 'time(.payload.metadata.lastUpdatedAt)'
```

To keep only your own validator's licence, extend `where` with your party id (`name::1220...`):

```toml
where = '.template_fqn == "splice-amulet:Splice.ValidatorLicense:ValidatorLicense" and .payload.validator == "your-validator::1220..."'
```

Many other examples name a DevNet party, participant or DSO id. Replace those with your own before you run them.

You can also start from a preview: add `--save my-filter.toml` to the `preview` command. It writes `name` (from the file name), `source` and `where`, with an empty `[select]` for you to fill in. It never overwrites a file.

## 4. Run it

```sh
cinder run validator-licenses.toml
```

`run` checks the filter first, so a mistake in it fails before PQS is contacted. It then reads from the oldest offset PQS holds and stops 1000 offsets later. Each kept record becomes one line on stdout. On our DevNet validator the first two lines are the create and the archive of one contract (ids shortened):

```json
{"_op":"upsert","_archived_at_offset":null,"_offset":112526,"_node_id":0,"_contract_id":"002e2ef7…","validator":"infradao-devnet-1::1220…","sponsor":"infradao-devnet-1::1220…","dso":"DSO::1220…","version":"0.7.4","contact_point":"","last_active_at":"2026-09-02T12:28:40.242546Z","metadata_updated_at":"2026-09-02T12:28:40.242546Z"}
{"_op":"upsert","_archived_at_offset":112864,"_offset":112864,"_node_id":0,"_contract_id":"002e2ef7…","validator":"infradao-devnet-1::1220…","sponsor":"infradao-devnet-1::1220…","dso":"DSO::1220…","version":"0.7.4","contact_point":"","last_active_at":"2026-09-02T12:28:40.242546Z","metadata_updated_at":"2026-09-02T12:28:40.242546Z"}
```

The reserved `_`-prefixed columns say which record a line comes from and how to load it (see [Documents](/filter-format.md#documents)). Your columns follow, in the order you wrote them. A `[select]` column name that starts with `_` fails validation.

On stderr, the run logs the range it reads (`running`), then the counts of records read, documents written and records skipped (`run complete`), then a cursor token (`resume from this cursor`). Without timestamps:

```
INFO cinder::run: running start=offset 112517 to=113517 name="validator_licenses" source=lifecycle
INFO cinder::run: `where` predicate applied at the source
INFO cinder::run: run complete records=7 emitted=7 skipped=0
INFO cinder::run: resume from this cursor cursor=eyJ2IjoxLCJvZmZzZXQiOjExMzM2Miwibm9kZV9pZCI6MX0
```

| To                         | Use                                           |
| -------------------------- | --------------------------------------------- |
| start somewhere else       | `--from OFFSET`                               |
| read further               | `--to OFFSET` or `--to +N`                    |
| continue where a run ended | `--cursor TOKEN`, with the token from the log |
| save the documents         | `> documents.ndjson`                          |

## 5. Deploy it to a node

To index the filter continuously, deploy it to a node and read its documents with SQL. That is the next part: [Tutorial: run the server](/server.md).

## Sources

* Example `validator-licenses.toml` at v0.9.0: <https://github.com/InfraDAO/canton-indexer-releases/blob/v0.9.0/examples/validator-licenses.toml>
* All examples at v0.9.0: <https://github.com/InfraDAO/canton-indexer-releases/tree/v0.9.0/examples>
* Filter format at v0.9.0 (quickstart, the "If you want to" table, the sample documents): <https://github.com/InfraDAO/canton-indexer-releases/blob/v0.9.0/docs/filter-format.md>
* Release notes: <https://github.com/InfraDAO/canton-indexer-releases/releases>
