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

# Storage

> A key/value store your workflows can read and write across runs — counters, cursors, de-duplication and locks, scoped to your workspace and to Live or Test.

# Storage

Everything else in a workflow run is temporary. Variables live for one execution; a
checkpoint belongs to that execution alone. **Storage** is the exception: a small
key/value store that outlives the run, so a workflow can remember something between
executions without you standing up a database.

It answers the three questions every real automation eventually asks:

* *Have I already processed this record?* — write the id, check it next time.
* *Where did I get to last time?* — keep a cursor.
* *Is another run already doing this?* — take a lock.

## Where your data lives

Storage entries belong to your workspace and to **one mode**. A key written by a Test
run and a key of the same name written by a Live run are two different entries: neither
can read or overwrite the other. That is the same separation Secrets, Routes and Clients
already have, and it means you can exercise a workflow in Test without touching the
counters and cursors your production automations depend on.

Inside one workspace and mode you can group keys with an optional **namespace**, so two
workflows can both use a key called `cursor` without colliding.

## Operations

| Operation     | What it does                                                                                        |
| ------------- | --------------------------------------------------------------------------------------------------- |
| **Get**       | Read a key. Returns your default value (or `null`) when it is absent or expired.                    |
| **Set**       | Write a key, creating it or replacing what was there.                                               |
| **Set if…**   | Compare-and-set. Write **only** if the key is free, or only if it currently holds a value you name. |
| **Increment** | Add to a counter, atomically. Negative amounts count down.                                          |
| **Push**      | Append an entry to a list.                                                                          |
| **Pop**       | Remove and return one entry — the last (a stack) or the first (a queue).                            |
| **Delete**    | Remove a key.                                                                                       |
| **List keys** | The key names in a namespace, with their sizes. Never their values.                                 |

### Why "Set if…" matters

**Set if…** is the operation that makes concurrency safe. KnoxCall runs your workflows on
several workers at once, so two runs really can reach the same step at the same moment. A
"read the value, then write it back" pattern loses one of the two updates whenever that
happens.

Set if… is decided by the database in a single step, so exactly one run can win:

* **Only if the key is free** (the default) — the winner gets `applied: true`, everyone
  else gets `applied: false` and can take the other branch. Add a **time to live** and you
  have a lock that releases itself if the holder dies.
* **Compare against an expected value** — turn off *Only if the key is free* and give the
  value the entry must currently hold. Useful for state machines: exactly one run moves
  `pending` to `claimed`.

**Increment** and **Push** are decided by the database in the same way, so a counter never
loses a tick and no list entry is ever dropped.

## Expiry

Give an entry a **time to live** and it stops being readable when that time passes — an
expired entry behaves exactly as if it had never been written, including for Set if…,
which is what makes an expired lock claimable again. Between 1 second and 90 days;
leave it blank for an entry that never expires.

## De-duplication, in three fields

The most common use. Before doing the work:

1. **Set if…** with the key `seen:{{trigger.body.id}}`, the value `true`, *Only if the key
   is free* on, and a time to live of a day or a week.
2. Add a **Condition** on `{{steps.dedupe.applied}}`.
3. Only the `true` branch does the work. A repeat delivery takes the other branch.

## Limits

|              |                                |
| ------------ | ------------------------------ |
| Key          | 256 bytes                      |
| Namespace    | 128 bytes                      |
| Value        | 64 KB of JSON                  |
| Keys         | 10,000 per workspace, per mode |
| List         | 1,000 entries                  |
| Time to live | 1 second – 90 days             |

Every limit **refuses** rather than trimming: a step that would exceed one fails with a
message naming the limit and the size it measured, and nothing is written. A value that
was silently shortened would be corruption that looked like success, and no key of yours
is ever evicted to make room for another.

If you need more than this, you want a real datastore — reach it with an **HTTP Request**
step and keep the connection details in a Secret.

## Never store a credential here

A storage value is data, not custody. KnoxCall refuses to publish a workflow whose step
configuration contains something credential-shaped, exactly as it does for HTTP headers
and email settings. Reference a Secret with `{{secrets.NAME}}` where the credential is
actually needed; it is injected on the wire and never stored in the workflow.

Values themselves are treated as your data throughout: they are never written to a run
log, and **List keys** returns names and sizes only.

## Billing

Each storage step counts as one **action** operation against your plan's monthly
allowance, like an HTTP or email step.
