Storage
Persistent key/value and list storage scoped to a workflow, for state that survives across runs.
The Storage node lets a workflow remember values between runs. State is scoped to a single workflow, so when a Cron Trigger fires every 5 minutes, each run sees the same persisted store.
The most common use is dedup. A sniper checks whether a mint has already been processed, acts only on new ones, and appends each handled mint to a list. The list survives across cron ticks without needing an external database.
Storage or Dataset?
Storage is a small key/value cache scoped to one workflow, with mandatory short TTLs. It is built for dedup and per-workflow state, not for accumulating history.
If you want to capture outputs across many workflows, keep them long enough to score over time, or query and aggregate them (counts, sums, averages), use the Dataset node instead. Datasets are account-scoped, long-retention, queryable tables, and any node can passively capture its output into one. A good rule of thumb: reach for Storage when you are asking "have I seen this before?", and for Datasets when you are asking "how did this perform?".
Prerequisites
- No API key required
Operations
| Operation | Purpose | Required fields |
|---|---|---|
| Get | Read the value at a key | key |
| Set | Write a value (creating or overwriting) | key, value (any JSON) |
| Has | Check whether a key exists, or whether a list contains a value | key (membership value optional) |
| Append Unique | Append to an array, no-op if already present | key, value (must be a string, number, or boolean, not an array or object) |
| Remove From List | Remove a value from an array | key, value (string, number, or boolean) |
| Delete | Delete the entire key | key |
| Host File → URL | Host base64 / data-URL image bytes as a fetchable URL | value (base64 or data: URL), no key |
Configuration
| Field | Type | Required | Description |
|---|---|---|---|
| Node Label | string | No | Display name shown on the canvas |
| Operation | enum | Yes | One of the operations above |
| Key | string | Most ops | Identifier for the stored value (max 128 chars). Allowed characters: letters, digits, _ - : . Not used by Host File → URL, which has no key. |
| Value | string | Varies | Required for Set, Append Unique, Remove From List, and Host File → URL (where it is the base64 / data: URL to host). |
| Value Shape | scalar | json | No | Controls how Value and List Membership Value are decoded. scalar (default) keeps the literal string. json parses the input. With json, Set stores any JSON value (arrays, numbers, booleans, objects), but Append Unique, Remove From List, and Has-with-membership only accept primitives (string, number, boolean) and reject arrays or objects |
| List Membership Value | string | No | Only for Has. When set, checks whether the array at the key contains this value |
| TTL Seconds | number | No | Only for Set and Append Unique. Overrides the default expiry. Must be a positive number, capped by your plan max (see Plan limits below) |
Key, Value, and List Membership Value all accept template expressions. For example:
seenMints
{webhook.payload.mint}
{token.address}Keep Value Shape consistent across reads and writes
If you store a value with Value Shape: json and the input is a number like 42, it is persisted as the number 42. A later Has check with Value Shape: scalar and the same 42 compares the string "42" against the number 42. The check returns false, and your dedup silently misses.
Rule of thumb: pick one shape (most workflows want scalar, since mint addresses and IDs are strings) and use the same shape for every Storage node touching that key. Switch to json only when you actually need numbers, booleans, or (for Set only) arrays and objects.
Plan limits
Storage is available on every plan. Caps differ by tier and are enforced server-side:
| Cap | Free (beta) | Pro / Ultra |
|---|---|---|
| Keys per workflow | 5 | 50 |
| Array entries per key | 250 | 1,000 |
| Value size | 5 KB | 10 KB |
| Default TTL | 48 hours | 48 hours |
| Max TTL | 48 hours (no permanent storage) | 7 days |
Free-tier values always expire. There is no permanent storage on the free plan. An hourly background sweep deletes expired entries.
Templating against the response
Each Storage operation writes its result to storageResponse (override via the Response Name field in the config dialog):
| Operation | Output shape |
|---|---|
| Get | { found: boolean, value: any, expiresAt: number | null } |
| Set | { ok: true, expiresAt: number } |
| Has | { exists: boolean } |
| Append Unique | { added: boolean, count: number, expiresAt: number } |
| Remove From List | { removed: boolean, count: number } |
| Delete | { deleted: boolean } |
| Host File → URL | { url: string, contentType: string, sizeBytes: number } |
All outputs include success: true and operation. Every operation except Host File → URL (which has no key) also includes key.
Operation details
Get
Reads the value at key. Returns found: false when the key is missing or expired. Expiration is lazy: the row is treated as absent on read regardless of whether the cron has swept it yet.
Set
Writes value at key, creating the row or overwriting it. The expiresAt resets on every write. If Value Shape is json, the value is parsed before storage so [1,2,3] becomes a real array, not the literal string.
Has
Two modes:
- Empty
List Membership Value: checks whether the key exists (and is not expired). - Non-empty
List Membership Value: checks whether the array at the key contains the value. If the stored value is not an array, falls back to scalar equality.
The List Membership Value's shape is also controlled by Value Shape. Pick json if the list contains numbers or booleans you want to match exactly. Whitespace-only resolved values (for example, a template that resolved to "") are treated as the empty case, so Has becomes a key-presence check.
Append Unique
Appends a primitive (string, number, or boolean) to the array at key. Arrays and objects are rejected at runtime. If you need those, use Set with Value Shape: json.
On a real append, the entry's TTL is reset to now + ttlSeconds (your override, or the 48h default), so a sliding dedup window stays alive as long as new items keep being appended.
If the value is already present, returns added: false and does not refresh the TTL. This prevents repeated Append Unique calls of the same value from keeping an entry alive past its dedup window.
If the key is missing or expired, a new array is created. If the key holds a non-array value (for example, you previously called Set with a string), the operation throws.
Remove From List
Removes a value from the array at key. No-op when the key is missing, expired, or the value is not present. Throws if the stored value is not an array. The TTL is not refreshed.
Delete
Deletes the entire row. No-op when the key is missing. Unlike Get and Has, which treat expired rows as absent, Delete operates on any row that still exists. This is useful for reclaiming a key slot before the hourly sweep runs.
Host File → URL
Turns raw image bytes into a fetchable URL so image fields on other nodes can point at them. Unlike every other operation, it has no key and never touches the key/value store: it takes a base64 string or a data: URL in the Value field, hosts the bytes, and returns a signed URL as {storageResponse.url}.
- Type is detected from the bytes. PNG, JPEG, WebP, GIF, and SVG are recognized from the content itself, so a missing or wrong
data:MIME label doesn't matter. Anything that isn't a supported image is rejected. - Max 16 MB after decoding.
Value ShapeandTTL Secondsdon't apply (the value is raw bytes, not a stored entry). - The URL is temporary: a signed link that lives about an hour, and the file is reaped within a day. Consume it in the same run, for example feed
{storageResponse.url}straight into Pump.funcreateTokenimageUrlor TelegramsendPhotophotoUrl. Don't stash it for later. - It does not count against your Storage caps. Keys-per-workflow, value size, and array limits below apply only to the key/value store, not to hosted files.
When your image comes from an AI node, that node already returns a hosted imageUrl directly (see AI > Image generation), so reach for this op when the bytes come from somewhere else, like an image API called through an HTTP node that returns base64.
Marketplace and ownership
Storage is scoped to (owner, workflowId). Marketplace clones receive a fresh, empty store, never the original author's data, so a buyer who imports a sniper template starts with zero "seen" mints. Storage entries are never shared between two workflows even if owned by the same user.
Common use cases
- Sniper or trading dedup: track mints you have already evaluated so a 1-minute cron does not re-process the same token forever
- "Already notified" lists for alerts and webhooks
- Per-workflow counters or recently-processed IDs
Example: dedup before buying a token
Cron Trigger -> Pump.fun (Get New) -> ForEach -> Storage (Has) -> Condition -> Pump.fun (Buy) -> Storage (Append Unique) -> ForEach EndInside the loop body:
- Storage (Has):
key: seenMints,List Membership Value: {token.mint}. Output:{exists: true | false}. - Condition: branch on
{storageResponse.exists}. Continue only whenfalse. - Pump.fun (Buy): execute the trade.
- Storage (Append Unique):
key: seenMints,value: {token.mint}, optionalTTL Seconds: 172800. The mint is now ignored on the next 48 hours of cron ticks (free default; lower it explicitly if you want a shorter dedup window).
For the inverse, marking a mint as eligible again after a sale, wire a Storage (Remove From List) node into your sell branch with the same key and value.
