# vgrid recipe

`vgrid recipe run` runs an [import recipe](https://docs.visigrid.app/guide/import-recipes/) without opening the app: it reads the recipe's source, applies its steps and checks, and writes the result. It uses the same code as the desktop app, so the same recipe and file give the same cells. Available from **VisiGrid 0.45.0**.

```
vgrid recipe run [OPTIONS] <RECIPE>
```

| Option | |
|--------|---|
| `--source <FILE>` | Read this file instead of the one the recipe names. |
| `-o, --output <FILE>` | Write the result: `.csv`, `.tsv`, `.json`, `.xlsx` or `.sheet`. Without it, CSV goes to stdout. |
| `--report <FILE>` | Also write the run report as JSON (written even when the run fails). |
| `-q, --quiet` | Print diagnostics only when the run fails. |

```bash
vgrid recipe run orders.recipe.toml -o orders.csv
vgrid recipe run orders.recipe.toml --source exports/october.csv -o october.xlsx
vgrid recipe run orders.recipe.toml --report run.json > orders.csv
```

The recipe's source path is relative to the recipe file. A pattern such as `export-*-*.csv` reads the most recently modified matching file in that folder, so a scheduled job picks up each new export without changes. With `combine = true` (0.48.0 and later) it reads every matching file and appends them, adding a **Source file** column; diagnostics then name the file as well as the line.

A `.json`, `.jsonl` or `.ndjson` source (0.51.0 and later) flattens nested objects into dotted columns such as `customer.name`; see [JSON files](https://docs.visigrid.app/guide/import-recipes/#json-files). For JSON Lines, diagnostics give the line of the record.

## Diagnostics

Diagnostics go to stderr: rows in and out for each step, columns missing or new since the recipe was saved (with likely renames), and each value that didn't fit its type, with its line number:

```
export-2026-10.csv (55d09d9d014ebab7): 4 source rows -> 3 rows x 4 columns, FAILED, nothing published
1. Keep columns Order Number, Customer, Amount, Order Date [4 -> 4 rows, 0 ms]: dropped 2 other columns
2. Rename Order Number → order_id [4 -> 4 rows, 0 ms]
3. Trim spaces in every column [4 -> 4 rows, 0 ms]
4. Set types Amount: number, Order Date: date:ymd, order_id: text [4 -> 4 rows, 0 ms] FAILED: 1 value did not fit
5. Keep rows where Amount > 0 [4 -> 3 rows, 0 ms]: removed 1 row
6. Remove duplicate rows [3 -> 3 rows, 0 ms]: removed 0 duplicate rows
  error: 1 value did not fit their columns' types
    line 5, Order Date: "10/03/2026" not a YYYY-MM-DD date
```

## Exit status

| Code | Meaning |
|------|---------|
| `0` | Every check passed; the result was written. |
| `70` | A check failed. Nothing was written, so an earlier output file is left as it was. |
| `71` | The recipe is invalid (unreadable, malformed, or an unsupported version). |

Output is written to a temporary file and moved into place, so a reader never sees a half-written file. The output and the report may not be the source, the recipe, or each other.

Recipes read only local regular files, up to 256 MB for a source and 1 MB for a recipe; network paths, pipes and devices are refused.

## VisiBooks sources

A recipe with `kind = "visibooks"` reads a report from VisiBooks (0.50.0 and later). It needs a read-only API key:

```bash
vgrid visibooks key              # paste the key; saved in the system keychain
vgrid visibooks entities         # the entity ids the key can read
vgrid visibooks key --delete
```

The key is stored for exactly one server (`--server`, default `https://api.visiapi.com`). For CI or a server without a keychain, set `VISIBOOKS_API_KEY` (and `VISIBOOKS_API_SERVER` for another server). `--source` can't replace a VisiBooks source with a file.