> Part of the walkerOS documentation. Project overview and full index: <https://www.walkeros.io/llms.txt>

# State

State is a built-in config property available on source, transformer, and destination steps. It replaces inline `$code:` for the two most common store operations: stash a value (`set`) and fetch a value (`get`). Both directions reuse the same [mapping](/preview/pr-749/docs/mapping.md) engine, so any value you can express in a mapping you can stash or fetch.

## Configuration[​](#configuration "Direct link to Configuration")

Add `state` to any step. It accepts a single operation or an array of operations:

```
"transformers": {

  "session": {

    "state": { "mode": "set", "key": "event.user.session", "value": "event.data.gclid" }

  }

}
```

### Properties[​](#properties "Direct link to Properties")

| Property  | Type             | Description                                                                             |
| --------- | ---------------- | --------------------------------------------------------------------------------------- |
| `mode`    | `'get' \| 'set'` | Direction. `get` reads from the store, `set` writes to it.                              |
| `store`   | `string`         | Optional store id from `flow.stores`. Defaults to the in-memory cache.                  |
| `key`     | `Mapping.Value`  | Resolves against `{ event, ingest }` to the **store key**.                              |
| `value`   | `Mapping.Value`  | `set`: the payload to store. `get`: where to write the fetched value.                   |
| `mapping` | `Mapping.Value`  | Optional. Shapes the value read (`get`) or the payload (`set`). Merged into the target. |

## Paths name their side[​](#paths-name-their-side "Direct link to Paths name their side")

`key` and `value` resolve against `{ event, ingest }`, so every path starts with the side it reads from:

* `event.` reads the event, for example `event.user.session` or `event.data.gclid`.
* `ingest.` reads the pipeline context a server source lifted from the request through `config.ingest`, for example a site id from the URL or an API key header.

A path without one of these prefixes never resolves. `walkeros validate` rejects it, and at runtime the entry logs a warning and is skipped.

`key` names the slot in the store. What `value` means depends on the mode:

* **`set`** resolves `value` to the **payload** to store. `value` can be any mapping value (a path, a constant, an `fn`, a `map`).
* **`get`** treats `value` as the **write target** for the fetched value. `event.x` writes onto the event. `ingest.x` writes into the ingest, where later steps and route matchers can read it but no destination ever receives it. It must be a string path (or a `ValueConfig` with a `key`); a pure constant, `fn`, `map`, `loop`, or `set` is rejected, and the path may not contain `*`.

```
// set: value is the payload (read from the event, written to the store)

{ "mode": "set", "key": "event.user.id", "value": "event.data.token" }



// get: value is the target (fetched from the store, written onto the event)

{ "mode": "get", "key": "event.user.id", "value": "event.data.token" }
```

An `ingest.` write is shared by every event in the scope and lands in place. When a step fans one input out into several events, an `ingest.` target does not keep a separate value per event; use an `event.` target when each event needs its own.

## The default store[​](#the-default-store "Direct link to The default store")

When `store` is omitted, state uses the same built-in in-memory store as [cache](/preview/pr-749/docs/collector/cache.md) (`__cache`). This store is an entry-capped LRU with a TTL sweep, so entries are **evictable**: treat the default store as best-effort, not durable. For persistence across restarts or instances, point `store` at a configured backing (filesystem, S3, GCS).

State keys on the default store are prefixed with `state:` so they cannot collide with cache entries that share the same store. Explicitly named stores use the key as-is.

An explicitly named store that is not declared in `flow.stores` is a validation error (`STATE_STORE_UNKNOWN`), reported by `walkeros validate` and by the deploy preflight. At runtime such an entry logs an error and is skipped; state never silently falls back to the default store for a named miss.

Stores declared with `file: true` hold raw bytes and are not supported by `state`. Naming one is a validation error (`STATE_STORE_FILE`), and at runtime the entry is skipped with a warning. Handle byte stores in a transformer instead.

## Execution order[​](#execution-order "Direct link to Execution order")

`state` normalizes to an array and entries run sequentially. Where the operations slot into the pipeline depends on the step type:

* **Source:** `before` chain → state → `next` chain → `collector.push`. All entries run before the collector receives the event, in array order. The source's `next` route is resolved after state ran, so it can route on a value a `get` loaded (for example `ingest.tier`). State is skipped when the source declares a `terminus`, which receives the raw event with no pipeline.
* **Transformer:** `before` chain → `get` → step mapping → `set` → `next` dispatch. `get` entries enrich the event before the mapping runs; `set` entries run after it settles, on every emitting path (straight-through, `next` routing, and per fork of a fan-out).
* **Destination:** `get` → mapping-to-payload push → `set` (after a successful send). A failed push skips the `set`. For a batched destination, an event that is enqueued (not yet delivered) does not trigger `set`; the batched `set`-on-flush path is not yet wired.

State is **fail-open**: if a store call or value resolution throws, the error is logged and the event passes through unchanged. The chain always continues. A key that does not resolve logs a warning and the event still passes through; a `get` that finds nothing in the store is silent.

## Shape what you read or write[​](#shape-what-you-read-or-write "Direct link to Shape what you read or write")

`mapping` runs a full [mapping](/preview/pr-749/docs/mapping.md) value over the value a `get` read from the store, or over the payload a `set` is about to write. One lookup can then fill several fields, with a default for each:

```
"transformers": {

  "loadUser": {

    "state": {

      "mode": "get",

      "store": "customers",

      "key": "event.user.id",

      "value": "event.user",

      "mapping": {

        "map": {

          "ltv": "ltv",

          "segment": { "key": "segment", "value": "unknown" }

        }

      }

    }

  }

}
```

With a stored row `{ "ltv": 1840, "segment": "loyal", "email": "x@y" }`, the event's `user` keeps its `id` and gains `ltv` and `segment`. The `email` column is never read onto the event.

* **Paths are relative to the value read**, not to `{ event, ingest }`. Inside `mapping`, `segment` means the stored row's `segment`.
* **Without `mapping` the whole value replaces the target, with `mapping` the result merges into it.** An object result merges into an object target and keeps the target's other keys. Anything else (a string, a number, an array, or a target that is not an object) replaces the target.
* **On a miss, only what you declared is written.** The mapping runs even when the store has nothing under the key, so declared fallbacks still apply. An empty result writes nothing and stays silent.

| Mapping on a miss                                    | Result                                                                                                                                                           |
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Fallback: `{ "key": "segment", "value": "unknown" }` | Writes `"unknown"`                                                                                                                                               |
| `condition`                                          | Receives `undefined`; a falsy return skips the rule (no fallback either), a truthy return continues                                                              |
| `validate`                                           | Receives what the rule resolved: the fallback with a `key` plus `value`, `{}` after a `map`, otherwise `undefined`; if it rejects, the fallback (if any) applies |
| `fn`                                                 | Receives `undefined`; a non-empty return is written                                                                                                              |
| A plain path such as `"segment"`                     | Resolves to nothing, nothing is written                                                                                                                          |

On a miss `fn` and `condition` receive `undefined`, so guard for it. An unguarded `v.segment` throws and logs an error on every miss.

A few more rules:

* **A `null` anywhere inside a picked object drops the whole object.** Picking `address` from `{ "address": { "street": "x", "zip": null } }` writes no `address`, while other picked fields still land. When the stored value contains `null` or other non-JSON values, the entry logs a warning.
* **The event's consent gates a mapping, never a field in the stored value.** A `consent` key inside the stored row does not unlock a consent-gated field. Each item of a `loop` is mapped with the loop's item mapping under the event's consent: a loop item's `consent` key neither unlocks nor holds back a value.
* **`set` with `mapping`** shapes the payload, then reads the stored entry, merges the result into it, and writes it back. This is not atomic: when two instances write the same key at once, the last writer wins. A stored value that is not an object is replaced, with a warning. A result that is not an object is written without a read.

## Examples[​](#examples "Direct link to Examples")

The first examples come from [`flow-complete.json`](https://github.com/elbwalker/walkerOS/blob/main/packages/cli/examples/flow-complete.json), the tested example flow. Its `enrich` sequence calls `sessionSave` on `session start` and `sessionLoad` on `order complete` (see [Routing](/preview/pr-749/docs/getting-started/flow/routing.md#conditional-next-and-sequences)).

### Save session data[​](#save-session-data "Direct link to Save session data")

`sessionSave` stashes the raw click id and the start time of a session in the default store:

`/flows/server/transformers/sessionSave/state`

```json
{
  "mode": "set",
  "key": [
    "event.user.session",
    "event.user.hash"
  ],
  "value": {
    "map": {
      "gclid": "event.data.gclid",
      "started": "event.timestamp"
    }
  }
}
```

state set remembers the gclid and start time of a session in the built-in cache. [Guide chapter `state-stores`](https://github.com/elbwalker/walkerOS/blob/main/packages/cli/examples/flow-complete.md#state-stores-state-stores-and-first-party-walkerjs)

The key is a list of paths: the session id when the browser has one, else the cookieless visitor hash. The value is a `map`, so one entry stores both fields:

`/flows/server/transformers/sessionSave/state/key`

```json
[
  "event.user.session",
  "event.user.hash"
]
```

The key falls back from the session id to the visitor hash. [Guide chapter `state-stores`](https://github.com/elbwalker/walkerOS/blob/main/packages/cli/examples/flow-complete.md#state-stores-state-stores-and-first-party-walkerjs)

`/flows/server/transformers/sessionSave/state/value`

```json
{
  "map": {
    "gclid": "event.data.gclid",
    "started": "event.timestamp"
  }
}
```

value maps what is stored. [Guide chapter `state-stores`](https://github.com/elbwalker/walkerOS/blob/main/packages/cli/examples/flow-complete.md#state-stores-state-stores-and-first-party-walkerjs)

session start stores gclid and start time under the session and visitor hash in the built-in cache; the event passes unchanged. The later order reads it (flow test).

`Event`

```json
{
  "name": "session start",
  "entity": "session",
  "action": "start",
  "data": {
    "id": "s3ss10n",
    "isNew": true,
    "count": 1,
    "marketing": true,
    "gclid": "gclid-abc123"
  },
  "id": "ev-session-start",
  "trigger": "load",
  "timestamp": 1700000000000,
  "consent": {
    "functional": true,
    "marketing": true
  },
  "globals": {
    "language": "en",
    "pageGroup": "content"
  },
  "user": {
    "device": "d3v1c3",
    "session": "s3ss10n",
    "hash": "5f1c0a9e7d3b2c41",
    "botScore": 0,
    "botCategory": "human"
  },
  "source": {
    "type": "express",
    "platform": "server",
    "url": "https://www.example.com/"
  }
}
```

`Out`

```javascript
return {
  "event": {
    "name": "session start",
    "entity": "session",
    "action": "start",
    "data": {
      "id": "s3ss10n",
      "isNew": true,
      "count": 1,
      "marketing": true,
      "gclid": "gclid-abc123"
    },
    "id": "ev-session-start",
    "trigger": "load",
    "timestamp": 1700000000000,
    "consent": {
      "functional": true,
      "marketing": true
    },
    "globals": {
      "language": "en",
      "pageGroup": "content"
    },
    "user": {
      "device": "d3v1c3",
      "session": "s3ss10n",
      "hash": "5f1c0a9e7d3b2c41",
      "botScore": 0,
      "botCategory": "human"
    },
    "source": {
      "type": "express",
      "platform": "server",
      "url": "https://www.example.com/"
    }
  }
}
```

Remember the session: example `sessionStart` of step `sessionSave`. [Guide chapter `state-stores`](https://github.com/elbwalker/walkerOS/blob/main/packages/cli/examples/flow-complete.md#state-stores-state-stores-and-first-party-walkerjs)

### Load it on the order[​](#load-it-on-the-order "Direct link to Load it on the order")

`sessionLoad` fetches the stored object back onto the order, where a destination maps `data.session.gclid`:

`/flows/server/transformers/sessionLoad/state`

```json
{
  "mode": "get",
  "key": [
    "event.user.session",
    "event.user.hash"
  ],
  "value": "event.data.session"
}
```

state get restores it on the order, per instance. [Guide chapter `state-stores`](https://github.com/elbwalker/walkerOS/blob/main/packages/cli/examples/flow-complete.md#state-stores-state-stores-and-first-party-walkerjs)

No session start was seen for this session in this instance, so nothing is restored and the event passes unchanged. With a stored session data.session carries the gclid (flow test).

`Event`

```json
{
  "name": "order complete",
  "entity": "order",
  "action": "complete",
  "data": {
    "id": "ORD-100",
    "total": 149.8,
    "currency": "EUR",
    "coupon": "WELCOME10"
  },
  "id": "ev-order-complete",
  "trigger": "load",
  "timestamp": 1700000000000,
  "consent": {
    "functional": true,
    "marketing": true
  },
  "globals": {
    "language": "en",
    "pageGroup": "checkout"
  },
  "user": {
    "device": "d3v1c3",
    "session": "s3ss10n",
    "id": "cust-42",
    "email": "jane@example.com",
    "hash": "5f1c0a9e7d3b2c41",
    "botScore": 0,
    "botCategory": "human"
  },
  "source": {
    "type": "express",
    "platform": "server",
    "url": "https://www.example.com/checkout/thanks"
  },
  "nested": [
    {
      "entity": "product",
      "data": {
        "id": "SKU-1",
        "name": "Trail Runner",
        "price": 129.9,
        "quantity": 1
      }
    },
    {
      "entity": "product",
      "data": {
        "id": "SKU-2",
        "name": "Running Socks",
        "price": 9.95,
        "quantity": 2
      }
    }
  ]
}
```

`Out`

```javascript
return {
  "event": {
    "name": "order complete",
    "entity": "order",
    "action": "complete",
    "data": {
      "id": "ORD-100",
      "total": 149.8,
      "currency": "EUR",
      "coupon": "WELCOME10"
    },
    "id": "ev-order-complete",
    "trigger": "load",
    "timestamp": 1700000000000,
    "consent": {
      "functional": true,
      "marketing": true
    },
    "globals": {
      "language": "en",
      "pageGroup": "checkout"
    },
    "user": {
      "device": "d3v1c3",
      "session": "s3ss10n",
      "id": "cust-42",
      "email": "jane@example.com",
      "hash": "5f1c0a9e7d3b2c41",
      "botScore": 0,
      "botCategory": "human"
    },
    "source": {
      "type": "express",
      "platform": "server",
      "url": "https://www.example.com/checkout/thanks"
    },
    "nested": [
      {
        "entity": "product",
        "data": {
          "id": "SKU-1",
          "name": "Trail Runner",
          "price": 129.9,
          "quantity": 1
        }
      },
      {
        "entity": "product",
        "data": {
          "id": "SKU-2",
          "name": "Running Socks",
          "price": 9.95,
          "quantity": 2
        }
      }
    ]
  }
}
```

Order without a stored session: example `orderWithoutSession` of step `sessionLoad`. [Guide chapter `state-stores`](https://github.com/elbwalker/walkerOS/blob/main/packages/cli/examples/flow-complete.md#state-stores-state-stores-and-first-party-walkerjs)

The default store is per instance. Behind a load balancer, point both steps at a shared store so the order finds the session saved on another instance.

### Read from a named store[​](#read-from-a-named-store "Direct link to Read from a named store")

`loadUser` reads the customer's record from the `customers` store, only for logged-in events. Its `mapping` picks `ltv` and `segment` from the stored row and merges them into `user`, so the `id` and `hash` already there stay:

`/flows/server/transformers/loadUser`

```json
{
  "state": {
    "mode": "get",
    "store": "customers",
    "key": "event.user.id",
    "value": "event.user",
    "mapping": {
      "map": {
        "ltv": "ltv",
        "segment": "segment"
      }
    }
  },
  "examples": { … }
}
```

loadUser puts the customer lifetime value and segment on logged-in events. [Guide chapter `state-stores`](https://github.com/elbwalker/walkerOS/blob/main/packages/cli/examples/flow-complete.md#state-stores-state-stores-and-first-party-walkerjs)

`/flows/server/transformers/loadUser/state/store`: `"customers"`. store reads from a named store instead of the built-in cache. [Guide chapter `state-stores`](https://github.com/elbwalker/walkerOS/blob/main/packages/cli/examples/flow-complete.md#state-stores-state-stores-and-first-party-walkerjs)

`/flows/server/transformers/loadUser/state/mapping`

```json
{
  "map": {
    "ltv": "ltv",
    "segment": "segment"
  }
}
```

mapping picks fields from the stored row and merges them into the target. [Guide chapter `state-stores`](https://github.com/elbwalker/walkerOS/blob/main/packages/cli/examples/flow-complete.md#state-stores-state-stores-and-first-party-walkerjs)

The customer id is looked up in the customers store (fs demo data, cached for five minutes) and its lifetime value and segment are merged into user, next to the id and hash already there.

`Event`

```json
{
  "name": "order complete",
  "entity": "order",
  "action": "complete",
  "data": {
    "id": "ORD-100",
    "total": 149.8,
    "currency": "EUR",
    "coupon": "WELCOME10"
  },
  "id": "ev-order-complete",
  "trigger": "load",
  "timestamp": 1700000000000,
  "consent": {
    "functional": true,
    "marketing": true
  },
  "globals": {
    "language": "en",
    "pageGroup": "checkout"
  },
  "user": {
    "device": "d3v1c3",
    "session": "s3ss10n",
    "id": "cust-42",
    "email": "jane@example.com",
    "hash": "5f1c0a9e7d3b2c41",
    "botScore": 0,
    "botCategory": "human"
  },
  "source": {
    "type": "express",
    "platform": "server",
    "url": "https://www.example.com/checkout/thanks"
  },
  "nested": [
    {
      "entity": "product",
      "data": {
        "id": "SKU-1",
        "name": "Trail Runner",
        "price": 129.9,
        "quantity": 1
      }
    },
    {
      "entity": "product",
      "data": {
        "id": "SKU-2",
        "name": "Running Socks",
        "price": 9.95,
        "quantity": 2
      }
    }
  ]
}
```

`Out`

```javascript
return {
  "event": {
    "name": "order complete",
    "entity": "order",
    "action": "complete",
    "data": {
      "id": "ORD-100",
      "total": 149.8,
      "currency": "EUR",
      "coupon": "WELCOME10"
    },
    "id": "ev-order-complete",
    "trigger": "load",
    "timestamp": 1700000000000,
    "consent": {
      "functional": true,
      "marketing": true
    },
    "globals": {
      "language": "en",
      "pageGroup": "checkout"
    },
    "user": {
      "device": "d3v1c3",
      "session": "s3ss10n",
      "id": "cust-42",
      "email": "jane@example.com",
      "hash": "5f1c0a9e7d3b2c41",
      "botScore": 0,
      "botCategory": "human",
      "ltv": 420,
      "segment": "loyal"
    },
    "source": {
      "type": "express",
      "platform": "server",
      "url": "https://www.example.com/checkout/thanks"
    },
    "nested": [
      {
        "entity": "product",
        "data": {
          "id": "SKU-1",
          "name": "Trail Runner",
          "price": 129.9,
          "quantity": 1
        }
      },
      {
        "entity": "product",
        "data": {
          "id": "SKU-2",
          "name": "Running Socks",
          "price": 9.95,
          "quantity": 2
        }
      }
    ]
  }
}
```

Known customer: example `knownCustomer` of step `loadUser`. [Guide chapter `state-stores`](https://github.com/elbwalker/walkerOS/blob/main/packages/cli/examples/flow-complete.md#state-stores-state-stores-and-first-party-walkerjs)

### Fetch a product margin before sending[​](#fetch-a-product-margin-before-sending "Direct link to Fetch a product margin before sending")

A destination reads a per-product margin from a store and writes it onto the event before mapping it to the vendor payload:

```
"destinations": {

  "warehouse": {

    "package": "@walkeros/server-destination-api",

    "state": { "mode": "get", "store": "catalog", "key": "event.data.product_id", "value": "event.data.margin" }

  }

}
```

### Route by a per-site registry[​](#route-by-a-per-site-registry "Direct link to Route by a per-site registry")

One endpoint serves many sites, and the URL is the only thing that says which one. The source lifts the site id from the path into ingest. A transformer looks up that site's row in a registry store and keeps it in ingest, so routing can use it without it reaching any destination:

```
"sources": {

  "collect": {

    "package": "@walkeros/server-source-express",

    "config": {

      "settings": { "paths": ["/:site/e"] },

      "ingest": {

        "map": { "site": { "fn": "$code:(req) => req.path.split('/')[1]" } }

      }

    },

    "next": "registry"

  }

},

"transformers": {

  "registry": {

    "state": { "mode": "get", "store": "registry", "key": "ingest.site", "value": "ingest.tenant" },

    "next": {

      "one": [

        {

          "match": { "key": "ingest.tenant.tier", "operator": "eq", "value": "enterprise" },

          "next": "enterprise"

        },

        "shared"

      ]

    }

  }

}
```

The lookup sits on a transformer, which runs its `get` before its `next` route. A source also runs its state before its `next` route resolves, so a source-level `get` can feed that source's routing too.

Destination mappings resolve against the event only, so `ingest.tenant` is not readable there. To put a registry field into the payload, target the event instead: `"value": "event.data.tenant"`.

## Related[​](#related "Direct link to Related")

* [Cache](/preview/pr-749/docs/collector/cache.md) short-circuits the pipeline on a hit; state always reads or writes and continues.
* [Stores](/preview/pr-749/docs/stores.md) provide the backing that state reads from and writes to.
* [Mapping](/preview/pr-749/docs/mapping.md) defines the `key` and `value` resolution.
