> ## Documentation Index
> Fetch the complete documentation index at: https://docs.suchi.page/llms.txt
> Use this file to discover all available pages before exploring further.

# Importing a Paperless-ngx export

> Review and import a Paperless-ngx export from the Suchi host or a one-shot Docker Compose container.

`suchi import --from ./export-bundle` consumes a Paperless-ngx export in its
JSON-manifest + `originals/` + `archive/` layout. The import is idempotent and
safe to re-run against a nightly export during a shadow window.

Select the existing destination with `--system S02` for both import and `--verify`;
omission uses original system 1. Bundle import does not introduce systems: use
[taxonomy import](/jd) first. Category/name resolution, metadata/workflow upserts,
legacy resume, ASN and duplicate lookup all stay in the target system. The owner
must be able to enter it. This Paperless/Django importer is not a native Suchi
takeout reader or a whole-instance backup restore.

Imported documents retain a `Paperless-ngx import` source with their original
filename. This provenance is independent from the preserved legacy id and does
not create a version or document link.

## Prerequisites

* A completed export bundle at `/path/to/export` produced by
  `document_exporter` (or an equivalent that emits the same
  `manifest.json` + `originals/` + `archive/` + `json/` tree).
* A running suchi instance with at least one user (usually the admin
  from `/setup`).
* Enough free disk for the CAS to absorb every unique document
  blob (originals + optional archive PDFs).

## Getting the dump out of paperless-ngx

Paperless-ngx ships `document_exporter`, a management command that
writes the exact tree suchi's importer reads.

For a docker-compose deployment:

```bash theme={null}
docker compose exec webserver \
    document_exporter -p -sm /usr/src/paperless/export
```

For a bare or systemd install:

```bash theme={null}
sudo -u paperless python manage.py \
    document_exporter -p -sm /path/to/export
```

Both flags matter:

* `-p` — folder-prefixed layout. Produces `originals/`, `archive/`,
  `thumbnails/`, `json/` under the export root instead of a flat dump.
* `-sm` — split manifest. Writes one JSON sidecar per document under
  `json/` instead of one huge `manifest.json`. Suchi reads either
  shape, but the split form is faster on large corpora and lets the
  export resume incrementally.

If paperless is in a container, copy the export off so the suchi host
can see it:

```bash theme={null}
docker cp \
    "$(docker compose ps -q webserver):/usr/src/paperless/export" \
    ./paperless-export
```

Bare-metal installs skip the copy — point `--from` at the export path
directly.

The resulting bundle looks like:

```
paperless-export/
  manifest.json    # workflows, saved views, users, groups, custom fields
  json/            # per-document sidecars (present when -sm was used)
  originals/       # source files
  archive/         # OCR'd PDFs (when paperless has produced them)
  thumbnails/      # optional, ignored by suchi
```

Two caveats worth knowing before you run the exporter:

* **Version.** Suchi's importer targets **paperless-ngx v3.0.5**
  (see [Migration target version](#migration-target-version)).
  Newer minor versions are not a supported compatibility surface —
  the report will flag unknown shapes as `Failed` with reason
  "paperless version drift" so you can decide what to do.
* **Duration.** `document_exporter` reads every original + every
  archive PDF end-to-end. On corpora over a few thousand docs, run
  it during a maintenance window; the exporter is safe to interrupt
  and re-run.

Once the bundle is on disk, review it before starting the import.

## Review, then import

Run a dry review first. It parses the manifest, verifies that each required
original exists, and reports what would be imported without writing to Suchi:

```bash theme={null}
suchi import --from /path/to/export --dry-run
```

Then run the confirmed import:

```bash theme={null}
suchi import --from /path/to/export \
  --owner-email you@example.com \
  --report ./import-report.md
```

The running web app sees imported rows immediately; neither command requires a
Suchi restart.

### Docker Compose

Use a one-shot container so a large export never passes through the browser or
reverse proxy. It shares Suchi's data volume and mounts the export read-only;
the running web container stays online:

```bash theme={null}
export EXPORT_DIR=/absolute/path/to/paperless-export

docker compose run --rm --no-deps \
  -v "$EXPORT_DIR:/imports/paperless:ro" \
  suchi import --from /imports/paperless --dry-run

docker compose run --rm --no-deps \
  -v "$EXPORT_DIR:/imports/paperless:ro" \
  suchi import --from /imports/paperless \
    --owner-email you@example.com \
    --report /data/import-report.md
```

The report remains in the `suchi-data` volume at
`/data/import-report.md`. Read it without restarting the service:

```bash theme={null}
docker compose exec suchi cat /data/import-report.md
```

The importer:

1. Loads every manifest object (top-level `manifest.json` plus any
   per-document sidecar files under `<root>/` or `<root>/documents/`).
2. Upserts tags, correspondents, document types, storage paths, and custom-field
   definitions **verbatim** within the selected system. Names are local identities —
   re-running there against a modified export updates that system's row only.
3. For each document, streams the original (and optional archive)
   file into the CAS by SHA-256, then inserts one `documents` row
   plus tag junctions and custom-field values in a single
   transaction. `legacy_id` is preserved for round-tripping
   and dedup.

## Dedup

Documents are keyed within the selected system by ingested-byte SHA-256/owner and
`legacy_id`. Re-running the same bundle against that system skips already imported
documents and reports them as `Skipped (dupes)`.

## Category resolution

By default, every imported document lands in the JD **inbox** —
visible, bulk-reassignable, never lost. Three flags override:

<Tabs>
  <Tab title="--flat">
    Force every doc to the inbox. Never consult a rule. Use this if
    you want your existing tag/correspondent process untouched —
    the inbox is your one bucket, JD affordances stay hidden in the
    UI.

    ```bash theme={null}
    suchi import --from ./export --owner-email you@ex.com --flat
    ```
  </Tab>

  <Tab title="--map-jd">
    Apply first-match user rules. Rules live in a TOML file:

    ```toml title="mapping.toml" theme={null}
    [[rules]]
    if = "tag:tax"
    category = 22

    [[rules]]
    if = "storage_path:Insurance"
    category = 23

    [[rules]]
    if = "document_type:Utility"
    category = 31

    [[rules]]
    if = "correspondent:Landlord"
    category = 32
    ```

    `if` is `<kind>:<name>` where kind is `tag`, `correspondent`,
    `document_type`, or `storage_path`. `category` is the JD code.

    ```bash theme={null}
    suchi import --from ./export --owner-email you@ex.com \
        --map-jd mapping.toml
    ```

    Unmatched docs → inbox.
  </Tab>

  <Tab title="--auto-jd">
    Apply the built-in heuristics — deterministic keyword matches
    against the starter tree (`tax` → 22 Tax, `insurance` → 23,
    `bescom`/`electricity`/`water` → 31 Utilities, `passport` /
    `aadhaar` → 11 Identity, and so on).

    ```bash theme={null}
    suchi import --from ./export --owner-email you@ex.com --auto-jd
    ```

    Bias: **high precision over recall**. A miss lands in inbox;
    silently mis-filing to the wrong category is worse than
    under-classifying.
  </Tab>
</Tabs>

<Info>The three flags are mutually exclusive — pick one strategy.</Info>

## Migration target version

The importer targets **paperless-ngx v3.0.5** and is not intended
to track that project's release train. Migration is a one-shot event —
picking a stable target once, freezing there, and treating anything
newer as "please tell us if you need it" is deliberately simpler than
maintaining a compatibility surface across paperless minor versions.

Unknown workflow trigger types, action types, or saved-view rule types
land as `FAILED` entries in the migration report with reason
"paperless version drift"; the report's follow-ups section lists each
kind so a future maintainer can decide whether the delta is worth a
bump.

## Workflows → automations

Paperless workflows migrate into suchi's [automations](/automations)
engine. Triggers, actions, and filter fields all map through a fixed
table.

**Trigger type mapping** (source `workflowtrigger.type`):

| Source              | Target trigger     | Outcome                                     |
| ------------------- | ------------------ | ------------------------------------------- |
| 1 CONSUMPTION       | `consumption`      | Full                                        |
| 2 DOCUMENT\_ADDED   | `document_added`   | Full                                        |
| 3 DOCUMENT\_UPDATED | `document_updated` | Full                                        |
| 4 SCHEDULED         | —                  | Failed — suchi has no scheduled automations |

**Action type mapping** (source `workflowaction.type`):

| Source              | Target action(s)                                                                                                                                                                                             | Outcome                                                                             |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
| 1 ASSIGNMENT        | fan-out to `assign_title` / `assign_tags` / `assign_correspondent` / `assign_document_type` / `assign_storage_path` / `assign_owner` / `assign_custom_field` per non-null field                              | Full                                                                                |
| 2 REMOVAL           | fan-out to `remove_tags` / `remove_correspondents` / `remove_document_type` / `remove_storage_path` / `remove_custom_field`; `remove_owner` is reported and dropped because Suchi documents require an owner | Full except reported partial mappings and truncated multi-value inputs              |
| 3 EMAIL             | —                                                                                                                                                                                                            | Partial — suchi has no `notify_email` verb yet; the report queues it as a follow-up |
| 4 WEBHOOK           | —                                                                                                                                                                                                            | Partial — no `webhook` verb yet                                                     |
| 5 PASSWORD\_REMOVAL | —                                                                                                                                                                                                            | Partial — no analog                                                                 |
| 6 MOVE\_TO\_TRASH   | `discard`                                                                                                                                                                                                    | Partial — `discard` skips the doc rather than soft-deleting                         |

**Trigger filter mapping**:

| Source field                                 | Target        | Outcome                                         |
| -------------------------------------------- | ------------- | ----------------------------------------------- |
| `filter_filename`                            | `filename`    | Full                                            |
| `filter_path`                                | `path`        | Full                                            |
| `filter_mailrule`                            | `mailrule_id` | Full                                            |
| `filter_has_tags` (first)                    | `tag_id`      | Full                                            |
| `filter_has_tags` (>1)                       | first only    | Partial — "any-of tags list truncated to first" |
| `filter_has_correspondent`                   | `corr_id`     | Full                                            |
| `filter_has_document_type`                   | `doctype_id`  | Full                                            |
| `filter_has_storage_path`                    | —             | Partial — no storage-path trigger filter        |
| `filter_has_all_tags`, `filter_has_not_tags` | —             | Partial — no negation / all-of semantics        |
| `filter_custom_field_query`                  | —             | Partial                                         |
| `matching_algorithm` (LITERAL/REGEX/FUZZY)   | —             | `match` dropped, Partial                        |

## Saved views

Paperless saved views migrate into suchi's `saved_views` — with a
lossy translation from paperless's 50-way `filter_rules.rule_type`
enum onto suchi's allow-listed `filter_json` keys (`q`, `tags__id__in`,
`correspondents__id__in`, `document_type__id`, `jd_category_id`,
`sensitivity`, `ordering`). Suchi still reads these flat imported filters; the
importer does not yet rewrite source rules into rich-query equivalents.

A view is `Full` when every rule maps 1:1; `Partial` when at least one
rule degrades (e.g., storage-path rule → text search); `Failed` when
zero rules map — the view row is skipped and reported.

**Rule-type mapping** (source `rule_type` int → target filter\_json):

| Source enum                                                          | Target                    | Outcome                                                                                                                                  |
| -------------------------------------------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| 0 title contains                                                     | `q`                       | Full                                                                                                                                     |
| 1 content contains                                                   | `q`                       | Partial (q matches title+content combined)                                                                                               |
| 2 ASN is / 18 not-has ASN / 23-24 ASN gt/lt                          | —                         | Failed — the importer does not translate ASN filters, although document ASN identity is retained within the target system                |
| 3 correspondent is                                                   | `correspondents__id__in`  | Full                                                                                                                                     |
| 4 document type is                                                   | `document_type__id`       | Full (multi → last wins, Partial)                                                                                                        |
| 5 is in inbox                                                        | —                         | Partial — suchi surfaces Inbox natively                                                                                                  |
| 6 has tag                                                            | `tags__id__in`            | Full                                                                                                                                     |
| 7 has any tag / 22 has tags in                                       | `tags__id__in`            | Full                                                                                                                                     |
| 8-16 date filters (created/added/modified before/after)              | —                         | Failed — importer does not translate source-system date rules; Suchi rich queries support `added:` and approved extracted `date:` values |
| 17 does not have tag                                                 | —                         | Failed — importer does not translate this rule to `-tag:`                                                                                |
| 19 title or content / 20 fulltext / 48 simple title / 49 simple text | `q`                       | Full                                                                                                                                     |
| 21 more like this                                                    | —                         | Failed                                                                                                                                   |
| 25 storage path is / 30 has storage path in                          | `q` text search           | Partial                                                                                                                                  |
| 26 has correspondent in                                              | `correspondents__id__in`  | Full                                                                                                                                     |
| 27, 29, 31 negative "in" filters                                     | —                         | Failed — importer does not translate negative metadata rules                                                                             |
| 28 has document type in                                              | `document_type__id` first | Partial                                                                                                                                  |
| 32-35 owner filters                                                  | —                         | Failed                                                                                                                                   |
| 36, 38-42 custom-field filters                                       | —                         | Failed                                                                                                                                   |
| 37 is shared by me                                                   | —                         | Failed                                                                                                                                   |
| 43-46 created/added to/from                                          | —                         | Failed                                                                                                                                   |
| 47 mime type is                                                      | —                         | Failed                                                                                                                                   |

Display mode: `TABLE` → `table`, `SMALL_CARDS` and `LARGE_CARDS` →
`card`. Icons are dropped (suchi doesn't render view icons).

## JD categorization

Choose at most one category strategy:

* `--flat` sends every imported document to the inbox.
* `--map-jd <file>` applies the first matching HuML or TOML rule.
* `--auto-jd` uses the bundled deterministic keyword mapping.

An unmatched rule or a rule targeting a category absent from the active tree
falls back to the inbox. Invalid targets are also listed in the import
warnings. The importer does not perform fuzzy matching or create approval
proposals.

## Migration report

Every non-dry-run import writes a markdown report to `--report
./import-report.md` (default). The report has these sections:

* **Header** — timestamp, source bundle root, target datadir,
  duration, active JD strategy.
* **Summary** — rows per model class (Documents / Tags /
  Correspondents / Document types / Storage paths / Custom fields /
  Notes / Workflows / Saved views), columns Full / Partial / Failed.
* **Partial migrations** — one row per Partial with source, target,
  reason.
* **Failed migrations** — one row per Failed with source, reason.
* **Follow-ups suggested** — aggregated feature gaps by blocker
  count, sorted descending, so unsupported source behavior is explicit.

The report is **the migration's system of record** — silent lossy
migrations feel like betrayal; every drop appears here with a reason.

## Verify (dry-diff)

The shadow-window primitive: diff a nightly export against the live
suchi DB without writing.

```bash theme={null}
suchi import --from ./nightly-export --verify
```

Report partitions every document in the bundle into:

* **New** — would be imported (no matching `legacy_id`).
* **Match** — present locally + compared fields all agree.
* **Differ** — present locally, but at least one compared field
  diverges. The report lists the field names.
* **Orphan** — present in suchi with a `legacy_id` that
  isn't in this bundle. Suggests a source-side deletion since the
  last export.

Compared fields: `title`, `original_size`. Deliberately minimal —
OCR text drifts trivially across source versions and would
false-positive.

## Shadow migration recipe

The recommended cutover for cautious operators:

<Steps>
  <Step title="Stand suchi up alongside the source system">
    Point it at the same backup pipeline.
  </Step>

  <Step title="Every night, verify">
    Run your DMS's exporter on the source side and `suchi import --verify` on
    the suchi side.
  </Step>

  <Step title="Watch the diffs settle">
    Any long-standing Differ or Orphan entries are the real migration surface to
    investigate.
  </Step>

  <Step title="Cut over">
    When diffs are boring, do a full `--auto-jd` (or `--map-jd`) import.
  </Step>

  <Step title="Repoint clients">
    Update mobile apps and staging-dir producers to point at suchi.
  </Step>

  <Step title="Cool off">
    Keep the source running for a two-week cool-off, then decommission.
  </Step>
</Steps>
