# File & export adapters

> Index mail, calendar, transcript, browser, and JSONL exports as individually searchable records, with no account access and no live connectors.

Section: Guides  
Canonical: https://gno.sh/docs/file-export-adapters  
Markdown: https://gno.sh/docs/file-export-adapters.md

An mbox file is not one document. It is four thousand messages that happen to share a file, and indexing it as a single blob makes every one of them unfindable. The same is true of a calendar export, a subtitle file, and a JSONL dump.

Export adapters split those containers into _logical records_: one message, one event, one cue, one bookmark, one row. Each becomes independently searchable and independently citable, while keeping its export path, exact source locator, dates, participants, thread or session identity, attachment inventory, and anchors.

These are **file adapters, not connectors**. They read export files you produced and placed somewhere. They do not authenticate to an account, inspect a live browser profile database or cookie store, fetch a URL, open an attachment, execute embedded content, or unpack an archive. If you want yesterday’s mail indexed, you export yesterday’s mail.

## What is supported

| Source                      | File types                   | One record is                               | Notes                                                                                                   |
| --------------------------- | ---------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| JSON Lines                  | `.jsonl`, `.ndjson`          | One valid object per line                   | Optional declarative field mapping; a malformed line is isolated rather than failing the file           |
| Mail                        | `.eml`, `.mbox`              | One message                                 | Bounded MIME nesting, body, and attachment counts; attachments are inventoried, never opened or indexed |
| Calendar                    | `.ics`                       | One VEVENT or recurrence exception          | Timezone-aware; recurrence anchors expand over a bounded local horizon rather than to infinity          |
| Transcripts                 | `.vtt`, `.srt`               | One cue or segment                          | Speaker and timestamp anchors retained                                                                  |
| Browser exports             | `.browser-export`            | One bookmark, history, or reading-list item | Must contain a recognized export shape; live profile databases are rejected outright                    |
| Explicit transcript exports | Configured `.json` or `.txt` | One segment or record                       | Opt-in only: generic JSON and text stay ordinary documents unless you declare otherwise                 |

Markdown, PDF, Office, plain text, and source code continue through the existing one-file-one-document converter lane. Nothing about adapters changes how those are handled.

## Getting started

For the automatic formats there is no configuration: add the export file or directory to a collection and index it.

```
gno collection add ~/exports --name exports \
  --pattern "**/*.{jsonl,eml,mbox,ics,vtt,srt}"
gno update
```

Collection `include` stays an extension allowlist: when it is non-empty, list the extensions you want. With the default empty `include`, an explicitly configured JSON transcript adapter automatically adds `.json` to the supported-extension scan.

### Mapping JSONL fields

GNO recognises conventional `id`, `title`, `text`/`content`/`body`, and `author` names. When your export uses different ones, declare a closed mapping:

```
collections:
  - name: exports
    path: /Users/me/exports
    pattern: "**/*"
    include: [.jsonl, .eml, .mbox, .ics, .vtt, .srt, .browser-export]
    recordAdapters:
      jsonl:
        fieldMapping:
          id: /external_id
          title: /subject
          body: /payload/text
          author: /owner/name
          participants: /participants
          threadId: /thread_id
          dateFields:
            created: /created_at
```

Selectors are JSON Pointers, or ordered arrays of JSON Pointers when a field lives in different places across rows. They are data, not code: a selector cannot execute, traverse prototypes, read a file, or make a network request.

### Opting into JSON or text transcripts

```
recordAdapters:
  transcript:
    format: json # json, text, vtt, or srt
```

Generic JSON and text are never guessed as transcripts. Heuristic detection here would misclassify ordinary config and data files, so it is per-collection opt-in.

## Identity, updates, and deletions

Each record gets an opaque key derived from the adapter identity plus the record’s stable export identity. Reimport with the same key and source hash is a no-op; a changed source hash updates the existing virtual document in place.

Deletion is where export ingestion usually goes wrong, so GNO is strict. A _complete_ snapshot deactivates records that disappeared from it. A _partial_ snapshot never does. A malformed row, a truncated file, invalid framing, or any cap failure marks the import partial, and a damaged export is therefore incapable of authorizing deletion. A half-written mbox cannot wipe your mail history from the index.

For JSONL, configure `fieldMapping.id` or provide a conventional `id` when updates must preserve identity. Without one, identity is derived from the row’s canonical content, so editing a row appears as one removal plus one addition rather than an in-place update. That is correct behaviour for content-addressed rows, but it will surprise you if you expected stable IDs, so set one.

## How records appear in results

Search and `gno get` report the real container path in `source.relPath`, plus a `record` object with the bounded locator and metadata. The unique `gno://` URI addresses GNO’s internal virtual document: use that URI or its docid with `gno get`.

Ask and Context Capsules carry the same record metadata alongside exact canonical-mirror line spans, so a claim can cite “message 14 of this mbox, lines 4 to 9” and be verified later. `record.adapter` holds the adapter ID, version, and configuration fingerprint that produced the record, which is what lets you tell “the export changed” apart from “my mapping changed”.

Virtual documents live under the reserved `.gno/records/` URI namespace, and a physical directory of that name is always excluded from collection walking so real files can never collide with the reserved space.

## What this is good for

- Searching a mail archive by meaning rather than by sender and date, with each message citable on its own.
- Asking what was decided in a meeting when the only record is a subtitle file.
- Pulling a calendar export into the same index as your notes, so “what did we agree the week of the offsite” has both halves available.
- Indexing an application’s JSONL log or event export as individually retrievable records without writing an ingestion script.
