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

# Store

> Persist keyed values across runs and pipelines using a managed store resource.

The **Store** action node reads from and writes to a managed store: an append-only, timestamped series of values indexed by a key. Use it to carry state across pipeline runs, compare current values against historical ones, or accumulate a running series without building a dedicated external database.

## When to use Store

* You want to **track how a value changes over time** across runs: prices, quantities, scores.
* You need to **compare the current run's data against the most recent previous value** without calling an external API.
* You want to **accumulate a running total or average** across multiple runs (e.g., daily volume, average invoice amount).
* A downstream [HTTP action](/nodes/http-action) or [Validation](/nodes/validation) node needs historical context that does not live in the current document.

For purely within-run named values, use [Variable](/nodes/variable) instead. Store is for values that must outlive a single run.

## Prerequisites

Before using a Store node, create at least one store under [Settings > Stores](/admin/stores). If no store exists, the node cannot be configured.

Every store carries a required **value schema** that describes the object held under each key. A Set node's value must match that schema, so design the store first and the node second.

## Modes

The Store node operates in one of two modes, selected in the node editor.

### Set

Appends the resolved `value` under the given `key`. The write is idempotent per run and node: if the same Store node runs twice for one pipeline run, only one entry is written.

| Field                  | Type   | Required | Description                                                                                                                                                     |
| ---------------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Store**              | select | Yes      | The managed store to write to                                                                                                                                   |
| **Key**                | text   | Yes      | The key to write under. Supports template expressions, e.g. `{{extract.payload.sku}}`                                                                           |
| **Value**              | JSON   | Yes      | A JSON object to store, written in the JSON editor. Map fields from upstream with template expressions, e.g. `{ "unitPrice": "{{extract.payload.unitPrice}}" }` |
| **Fail on null value** | toggle | No       | When on, the run fails if any part of the value comes back empty (see below). Default off                                                                       |

The value must be a **JSON object**, not a scalar or an array: a pipeline whose Set value is anything else fails validation on save. The object must also match the store's value schema. Ingestly adds a reserved `key` property carrying the entry key to every stored value, so never declare `key` in the schema or in the node's value.

A value larger than 256 KB is rejected and the step fails.

#### Fail on null value

By default, an expression that resolves to nothing renders as an empty string inside the stored object and the run succeeds. Turn **Fail on null value** on to make that a hard failure instead. The check covers three cases and names what went wrong:

* An **unresolved reference**: the message quotes the token, for example `the reference {{extract.payload.total}} did not resolve`.
* A **JSON-null leaf**, named by its dotted path, for example `'lines[0].sku' resolved to a null or empty value`.
* An **empty or whitespace-only string leaf**, named by that same dotted path.

Built-in helpers such as `{{uuid}}` and `{{now}}` always resolve, so they are never reported as unresolved. An empty object or an empty array is not a leaf and does not trip the check.

#### Set inside a Loop

<Note>A Set inside a [Loop](/nodes/loop) body is buffered, not written straight away. Buffered writes are committed together only after every item succeeds, so a loop that fails part-way persists nothing. A Get in the same loop reads committed entries only: it never sees the loop's own pending writes.</Note>

### Get

Reads the series for a key and returns the result as the node's payload. Three operations are available.

| Field                   | Type    | Required | Description                                                                                                                                                                                  |
| ----------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Store**               | select  | Yes      | The managed store to read from                                                                                                                                                               |
| **Key**                 | text    | Yes      | The key to read. Supports template expressions                                                                                                                                               |
| **Operation**           | select  | Yes      | What to return: `Latest`, `Last N`, or `Aggregate`                                                                                                                                           |
| **Window**              | section | No       | Optional time filter: `Relative` or `Range` (see below)                                                                                                                                      |
| **Fail when not found** | toggle  | No       | When on, the run fails if the read matches nothing, for example an unknown key or a window with no entries. Default off, so a miss succeeds with a `null` payload and `found` set to `false` |

#### Latest

Returns the single most recent entry for the key. The result lands at `payload`. Stored JSON is parsed back into structured data, so read its fields directly, for example `{{getPrice.payload.unitPrice}}`. `metadata.store.found` is `false` and `payload` is `null` when no entry matched.

#### Last N

Returns the most recent N entries as a list. The result lands at `payload` (array, newest first). Empty array when no entries matched.

| Field | Type   | Required | Description                                            |
| ----- | ------ | -------- | ------------------------------------------------------ |
| **N** | number | Yes      | How many recent entries to return, between 1 and 1,000 |

#### Aggregate

Computes a number over the series. The result lands at `payload` (number). An empty series yields `0` for `Total` and `Count`, and `null` for `Average`, `Lowest`, and `Highest`.

| Field                  | Type   | Required | Description                                                                                                                                                                     |
| ---------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Aggregate function** | select | Yes      | `Average`, `Total`, `Lowest`, `Highest`, or `Count`. **Average** is preselected when you pick Aggregate, and an older configuration that never chose a function runs as Average |
| **Value path**         | text   | No       | The path to the numeric field inside each stored entry, e.g. `amount` or `items[0].price`. Shown for every function except `Count`                                              |

The node reads **Value path** out of each stored entry before computing the aggregate. Entries where the path does not resolve to a number are skipped. Leave it blank only when the stored value is itself a number: values written by a Set node are always objects, so a blank path aggregates nothing for them.

<Warning>**Value path is a literal path, not a template expression.** It addresses the stored value's own shape, so use the entry's field names with dots for nested objects and brackets for array elements (`amount`, `data.amount`, `items[0].price`). A `{{token}}` there is rejected when you save the pipeline, and a pipeline saved before this rule fails the step at run time with the same message rather than aggregating silently to `0` or `null`.</Warning>

The editor suggests paths sampled from entries already stored under the configured key. Click a suggestion to fill the field. Upstream pipeline fields are deliberately not offered, because the path is read against the stored entry rather than the current run.

#### Window

All three operations accept an optional **Window** to restrict which entries are included.

| Window type  | Fields       | Description                                                                                                 |
| ------------ | ------------ | ----------------------------------------------------------------------------------------------------------- |
| **Relative** | Amount, Unit | Entries from the last N hours, days, weeks, or months. Amount is between 1 and 10,000                       |
| **Range**    | From, To     | Entries between two timestamps. Both fields support template expressions, e.g. `{{vars.payload.startDate}}` |

<Warning>**Range bounds are UTC only.** A timestamp carrying a non-zero offset (for example `2026-01-01T10:00:00+05:30`) is rejected at run time and the step fails. A timestamp with no zone at all (`2026-01-01T10:00:00`) is read as UTC.</Warning>

When no window is set, all entries for the key are included.

## Output

The node's data lives in `payload` and identity/provenance fields are under `metadata.store`. Read downstream as `{{storeName.payload}}` or `{{storeName.metadata.store.<field>}}` where `storeName` is the node name in your pipeline.

**`payload`** - the operation result:

| Field                         | Present when | Description                                                                                                                                                                                                       |
| ----------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| *(root, boolean)*             | Set mode     | `true` when the entry was written, or held by an enclosing Loop for commit after the loop succeeds. `false` when an internal retry replayed a write that had already committed, so nothing new was appended       |
| *(root, object/array/number)* | Get mode     | The retrieved value: the most recent entry (Latest), an array of entries newest-first (Last N), or the computed number (Aggregate). Stored JSON is parsed back into structured data. `null` when no entry matched |

**`metadata.store`** - identity and provenance:

| Field     | Present when | Description                                                                                                                                            |
| --------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `storeId` | Always       | The ID of the store that was accessed                                                                                                                  |
| `key`     | Always       | The resolved key that was read or written                                                                                                              |
| `found`   | Get mode     | `true` when at least one entry matched the key (and window, if set)                                                                                    |
| `count`   | Get mode     | Entries behind the result, after window filtering: `1` for a Latest hit, the number returned for Last N, the number of values aggregated for Aggregate |

<Note>`found` and `count` are present for **every** Get shape, including a miss and an empty aggregate series, where `count` is `0`. A downstream node can branch on `{{getPrice.metadata.store.count}}` without first checking that the field exists.</Note>

## Limits

The pipeline editor enforces the same bounds the API publishes, so a configuration the editor accepts is not refused when you save.

| Limit                  | Value                                             |
| ---------------------- | ------------------------------------------------- |
| Key length             | 256 characters, after the key expression resolves |
| Value size             | 256 KB per entry                                  |
| **N** (Last N)         | 1 to 1,000                                        |
| Relative window amount | 1 to 10,000                                       |

## When a store is missing

Deleting a store that a pipeline still references breaks that pipeline:

* The node editor shows **Store not found** in place of the store name, with an inline note telling you the selected store no longer exists in this workspace.
* Saving or validating the pipeline reports a validation error naming that node, and the pipeline stays invalid (and therefore cannot run) until you point the node at a store that exists.
* If the pipeline does run against a deleted store, the step fails with `Store not found or was deleted.`

## Moving a pipeline between workspaces

Pipeline exports carry store references **by name**, not by ID. On import, Ingestly rebinds each Store node to the store with the same name in the destination workspace. When no store matches that name (or more than one does), the reference is cleared and reported: the import warning lists unbound connectors, stores, and forward targets together, so open each affected node and pick a replacement. See [export and import](/pipelines/editor#export-and-import).

## Example: price change detection

A purchase-order pipeline captures each line item's unit price on every run and surfaces a warning when the price has changed.

**Set node** (runs when the order arrives):

```
Store: prices
Key: {{extract.payload.sku}}
Value: {
  "unitPrice": "{{extract.payload.unitPrice}}"
}
```

**Get node** (runs in the next pipeline run for the same SKU):

```
Store: prices
Key: {{extract.payload.sku}}
Operation: Latest
```

**Downstream Variable node** computes the delta:

```
priceChanged: {{getPrice.metadata.store.found && getPrice.payload.unitPrice != extract.payload.unitPrice}}
previousPrice: {{getPrice.payload.unitPrice}}
```

A downstream [Validation](/nodes/validation) node can then gate on `{{vars.payload.priceChanged}}`, or an edge condition can branch on it. [Filter](/nodes/filter) is not the node for this: it only keeps or drops pages of one document and never branches on a variable.

## Inputs and outputs

**Allowed inputs:** All trigger nodes and all action nodes.

**Output:** The node result with data at `payload` and identity fields at `metadata.store`, as described in the Output section above.

## Credits

Store nodes are **free**. They consume 0 credits per execution.

## Related

<CardGroup cols={2}>
  <Card title="Stores settings" icon="database" href="/admin/stores">
    Create and manage store resources
  </Card>

  <Card title="Variable" icon="brackets-curly" href="/nodes/variable">
    Define named values that live only within a single run
  </Card>

  <Card title="HTTP action" icon="globe" href="/nodes/http-action">
    Call external APIs to read or write data mid-pipeline
  </Card>

  <Card title="Expressions and filters" icon="braces" href="/guides/expressions">
    Template syntax for key and value fields
  </Card>
</CardGroup>
