skills/walkeros-understanding-stores/SKILL.md
Use when working with walkerOS stores, understanding key-value storage in flows, or learning about store injection via env. Covers interface, lifecycle, $store. wiring, and available store packages.
npx skillsauth add elbwalker/walkeros walkeros-understanding-storesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Stores provide key-value storage that other components (sources, transformers, destinations) consume via environment injection. They are the 4th component type in Flow.Json alongside sources, transformers, and destinations.
Core principle: Stores are passive infrastructure. They don't process events or participate in chains — they provide state that other components read and write.
See packages/core/src/types/store.ts for the canonical interface.
| Property | Type | Purpose | Required |
| --------- | ---------------------------------- | ---------------------------- | ------------ |
| type | string | Store type identifier | Required |
| config | Store.Config | Settings and env | Required |
| get | (key) => StoreValue \| undefined | Read a value | Required |
| set | (key, value, ttl?) => void | Write a value (optional TTL) | Required |
| delete | (key) => void | Remove a value | Required |
| destroy | DestroyFn | Cleanup on shutdown | Optional |
All methods can be sync or async (return Promise).
file modeStores hold one canonical value type: structured data (StoreValue), with
binary (Uint8Array) as a first-class leaf. StoreValue is
string | number | boolean | null | Uint8Array | StoreValue[] | { [key: string]: StoreValue }
(undefined is reserved as the "miss" sentinel and is never a stored value). A
shared core codec (serializeStoreValue / deserializeStoreValue) round-trips
that value to and from each backing.
Store.Config.file?: boolean (default false) picks the mode, decided once at
init:
StoreValue data, serialized by the
shared codec.file: true): a byte-native backend (fs, S3, GCS) persists raw
bytes byte-exact. set() accepts a Uint8Array or string and stores it
untouched; get() hands the exact bytes back. Use for serving assets such as
walker.js. The Sheets store is structured-only and rejects file: true at
init.TTL is owned by the cache layer, not the store. The store persists values; a
cache wrapper manages expiry. flow_validate warns when a store sets both
file: true and cache, and when a @walkeros/server-transformer-file is
wired to a byte-native store that does not set file: true.
Stores use the same context-based init pattern as other components:
import type { Store } from '@walkeros/core';
export const storeMyStore: Store.Init = (context) => {
const { config, env, logger, id } = context;
const settings = config.settings || {};
return {
type: 'my-store',
config: context.config as Store.Config,
get(key) {
/* ... */
},
set(key, value, ttl) {
/* ... */
},
delete(key) {
/* ... */
},
destroy() {
/* cleanup */
},
};
};
Context contains:
| Property | Type | Purpose |
| ----------- | -------------------- | ------------------------- |
| config | Store.Config | Settings from flow config |
| env | Store.Env | Environment dependencies |
| logger | Logger.Instance | Scoped logger |
| id | string | Store identifier |
| collector | Collector.Instance | Reference to collector |
Stores have the simplest lifecycle of all component types:
Startup: Stores → Destinations → Transformers → Sources
Shutdown: Sources → Destinations → Transformers → Stores
require or
deferred activation — they are always eager$store.Use $store.storeId in a component's env to inject a store instance:
{
"stores": {
"data": {
"package": "@walkeros/server-store-fs",
"config": { "settings": { "basePath": "./data" } },
"cache": { "rules": [{ "ttl": 60 }] }
}
},
"transformers": {
"fingerprint": {
"package": "@walkeros/server-transformer-fingerprint",
"env": { "store": "$store.data" }
}
}
}
The bundler resolves $store.data to a runtime reference. Invalid references
are caught at build time. walkeros validate also catches typos at validation
time, including unknown store names and the colon-instead-of-dot mistake (e.g.
$store:data is flagged with the suggested form $store.data).
The cache field enables the built-in in-memory cache tier (__cache) on top
of any backing store. No separate memory store declaration is needed.
Pass store instances directly — no $store. prefix needed:
import { startFlow } from '@walkeros/collector';
import { storeFsInit } from '@walkeros/server-store-fs';
import { transformerFingerprint } from '@walkeros/server-transformer-fingerprint';
const { collector } = await startFlow({
stores: {
data: {
code: storeFsInit,
config: { settings: { basePath: './data' } },
cache: { rules: [{ ttl: 60 }] },
},
},
transformers: {
fingerprint: {
code: transformerFingerprint,
env: { store: collector.stores.data }, // Direct reference
},
},
});
Note: In integrated mode, you wire the store instance directly in env rather
than using the $store. string prefix (that's a bundler feature).
__cache)The collector ships a built-in in-memory cache with LRU eviction, TTL, and
entry/byte caps. Enable it on any store by setting Flow.Store.cache. No
separate package import needed:
{
"stores": {
"files": {
"package": "@walkeros/server-store-fs",
"cache": { "rules": [{ "ttl": 60 }] }
}
}
}
Use this for the common "cache in front of a slow backing store" pattern (API, GCS, Sheets, etc.).
@walkeros/server-store-fs (filesystem)File-based store for serving static assets. Server-only.
import { storeFsInit } from '@walkeros/server-store-fs';
@walkeros/server-store-s3 (S3-compatible object storage)S3-compatible store using s3mini (~20 KB, zero dependencies). Works with AWS
S3, Cloudflare R2, Scaleway, DigitalOcean Spaces, Backblaze B2, MinIO, and any
S3-compatible provider. Structured by default (stored as application/json);
set file: true to serve raw bytes byte-exact with the real mime. Server-only.
import { storeS3Init } from '@walkeros/server-store-s3';
Settings:
| Setting | Type | Required | Default | Purpose |
| ----------------- | -------- | -------- | -------- | -------------------------- |
| bucket | string | Yes | — | S3 bucket name |
| endpoint | string | Yes | — | S3-compatible endpoint URL |
| accessKeyId | string | Yes | — | S3 access key ID |
| secretAccessKey | string | Yes | — | S3 secret access key |
| region | string | No | "auto" | AWS region (SigV4 signing) |
| prefix | string | No | — | Key prefix for scoping |
Primary use case: Serving static files in managed deployments (Mode D) where files live in a bucket rather than being baked into a Docker image.
@walkeros/server-store-gcs (Google Cloud Storage)Zero-dependency GCS store using raw fetch + GCS JSON API. Built-in auth: ADC
on Cloud Run / GKE, or explicit service account JWT. Server-only.
import { storeGcsInit } from '@walkeros/server-store-gcs';
Settings:
| Setting | Type | Required | Default | Purpose |
| -------- | -------- | -------- | ------- | ---------------------- |
| bucket | string | Yes | — | GCS bucket name |
| prefix | string | No | — | Key prefix for scoping |
Credentials live at config.credentials (sibling of settings):
string | object SA JSON for non-GCP envs, $env-resolvable; omit for ADC. The
deprecated settings.credentials still works.
Primary use case: Serving static files on GCP infrastructure (Cloud Run, GKE) where ADC provides seamless authentication.
@walkeros/server-store-sheets (Google Sheets)Zero-dependency Google Sheets store using raw fetch + Sheets v4 REST API. One
row per key, one cell per value (JSON-serialized). Built-in auth shared with the
GCS store. Server-only. Structured-only: cells hold structured JSON, so it
rejects file: true at init and rejects values carrying a binary (Uint8Array)
leaf. Use fs, S3, or GCS for byte-exact serving.
import { storeSheetsInit } from '@walkeros/server-store-sheets';
Settings:
| Setting | Type | Required | Default | Purpose |
| ------------ | -------- | -------- | ---------- | -------------------------------- |
| id | string | Yes | — | Spreadsheet ID (segment in URL) |
| sheet | string | No | 'Sheet1' | Sheet (tab) name |
| key | string | No | 'A' | Column letter for keys |
| value | string | No | 'B' | Column letter for JSON values |
| headerRows | number | No | 1 | Header rows to skip when reading |
Credentials live at config.credentials (sibling of settings):
string | object SA JSON for non-GCP envs, $env-resolvable; omit for ADC. The
deprecated settings.credentials still works.
Primary use case: Demos and small prototypes where the spreadsheet is the
operator-facing UI for tweaking lookup data. Quota: 60 reads/min and 60
writes/min per project. Enable the built-in cache via Flow.Store.cache on the
store declaration to absorb the quota, otherwise quota burns in seconds. Not a
production CRM substitute. See
Website: Sheets Store for the
cache-wiring example.
Declare any store consumed by one or more components in the stores section of
the flow config and wire it via $store.<id> in component env. The built-in
cache tier (Flow.Store.cache) covers the "fast in-memory cache in front of a
slow backing store" pattern without a separate memory store.
After startFlow(), stores are available on the collector instance:
const { collector } = await startFlow({
stores: {
files: { code: storeFsInit, config: { settings: { basePath: './data' } } },
},
});
// Read/write
await collector.stores.files.set('key', 'value', 60000); // 60s TTL
const value = await collector.stores.files.get('key');
await collector.stores.files.delete('key');
Store operations (get, set, delete) are wrapped with useHooks during
initialization, enabling pre/post interception via the collector's hooks system.
Available hook names:
| Hook name | Pre hook | Post hook |
| ------------- | ---------------- | ----------------- |
| StoreGet | preStoreGet | postStoreGet |
| StoreSet | preStoreSet | postStoreSet |
| StoreDelete | preStoreDelete | postStoreDelete |
Hooks fire on every store operation regardless of which component triggered it
(cache system, transformer via env, destination via env, direct access on
collector.stores).
const { collector } = await startFlow({
stores: {
files: { code: storeFsInit, config: { settings: { basePath: './data' } } },
},
});
// Intercept all store reads
collector.hooks.preStoreGet = ({ fn }, key) => {
console.log('Reading key:', key);
return fn(key);
};
| Aspect | Sources/Transformers/Destinations | Stores |
| ------------- | --------------------------------- | ------------------------ |
| Event flow | Participate in push chain | No push, no chain |
| next/before | Chain connection fields | None (passive) |
| Lifecycle | Init after stores | Init first, destroy last |
| require | Deferred activation supported | Always eager |
| Interface | push(event, context) | get/set/delete(key) |
dev.ts)Every store package must export schemas and examples via dev.ts, matching the
convention used by sources and destinations:
src/
├── schemas/
│ ├── settings.ts # Zod schema for store settings
│ └── index.ts # Re-export + zodToSchema() conversion
├── examples/
│ └── index.ts # Example Store.Config objects
└── dev.ts # export * as schemas; export * as examples
// src/schemas/settings.ts
import { z } from '@walkeros/core/dev';
export const SettingsSchema = z.object({
mySetting: z.string().describe('Setting description for docs and MCP'),
});
export type Settings = z.infer<typeof SettingsSchema>;
// src/schemas/index.ts
import { zodToSchema } from '@walkeros/core/dev';
import { SettingsSchema } from './settings';
export { SettingsSchema, type Settings } from './settings';
export const settings = zodToSchema(SettingsSchema);
// tsup.config.ts
import { defineConfig, buildModules, buildDev } from '@walkeros/config/tsup';
export default defineConfig([buildModules(), buildDev()]);
Add ./dev export to package.json:
"./dev": {
"types": "./dist/dev.d.ts",
"import": "./dist/dev.mjs",
"require": "./dist/dev.js"
}
buildDev() generates dist/walkerOS.json at build timewalkerOS.json from CDN for package discovery and schema
validation<PropertyTable schema={schemas.settings} /> instead of
hardcoded markdown tablesdev.ts, a store is invisible to MCP and docs tables rotStores can optionally export hints — lightweight, actionable context for AI
agents beyond what schemas and examples already provide. Create src/hints.ts:
import type { Hint } from '@walkeros/core';
export const hints: Hint.Hints = {
'persistence-behavior': {
text: 'Describes persistence guarantees. See settings schema for options.',
},
};
Export from src/dev.ts alongside schemas and examples:
export * as schemas from './schemas';
export * as examples from './examples';
export { hints } from './hints';
Most stores don't need hints — only add them for non-obvious behaviors, prerequisites, or troubleshooting patterns.
Stores can implement an optional setup() lifecycle to provision external
resources, for example creating a SQLite table, initializing an S3 bucket, or
running a one-off schema migration. Setup is never invoked by the runtime,
push, init, or deploy. It runs only when an operator explicitly types
walkeros setup store.<name>.
The signature is
(ctx: LifecycleContext<Config<T>, Env<T>>) => Promise<unknown>, where
LifecycleContext carries { id, config, env, logger }. Idempotency is the
package's responsibility: the framework adds no opinion. Use
resolveSetup(ctx.config.setup, DEFAULTS) from @walkeros/core to normalize
the boolean | object shape into a concrete options object.
See walkeros-create-destination,
walkeros-create-source,
walkeros-understanding-destinations,
walkeros-understanding-sources,
and the walkeros setup CLI documentation for the authoring template and
operator workflow.
state block for fetch/stash without
$code:Source files:
Documentation:
testing
Use when wiring `@walkeros/transformer-ga4` into a server flow, overriding default GA4 event mappings, dropping events, adding custom event keys, or troubleshooting GA4 Measurement Protocol decoding. Covers the `before`-chain wiring contract, configuration recipes, and per-field patching with extend/remove.
testing
Use when writing, simulating, validating, or testing with walkerOS step examples. Covers the complete lifecycle from authoring examples to CI integration.
tools
Use when bundling walkerOS flows, testing events with simulate/push, running local servers, validating configs, or configuring Flow JSON files.
data-ai
Use when working with walkerOS sources, understanding event capture, or learning about the push interface. Covers browser, dataLayer, and server source patterns.