.agents/features/store-entry.md
The Store Entry feature provides a persistent key-value store that piece steps can read from and write to during flow execution. It is a backend-only feature — there is no user-facing UI. Each entry is scoped to a project and identified by a string key. Values are arbitrary JSON (stored as jsonb). The API is exclusively accessible by the flow engine (worker), using securityAccess.engine() on all three endpoints. Pieces use SDK helpers (storage.get, storage.put, storage.delete) which call these endpoints at runtime. The maximum key length is 128 characters and the maximum serialized value size is 512 KB.
packages/server/api/src/app/store-entry/store-entry.controller.ts — REST controller (upsert, get, delete)packages/server/api/src/app/store-entry/store-entry.service.ts — CRUD servicepackages/server/api/src/app/store-entry/store-entry-entity.ts — TypeORM entitypackages/server/api/src/app/store-entry/store-entry.module.ts — Fastify plugin registrationpackages/shared/src/lib/core/store-entry/store-entry.ts — StoreEntry type, STORE_KEY_MAX_LENGTH, STORE_VALUE_MAX_SIZEpackages/shared/src/lib/core/store-entry/dto/store-entry-request.ts — PutStoreEntryRequest, GetStoreEntryRequest, DeleteStoreEntryRequestvalue field is unknown (any JSON-serializable value).key column and in the request schema.object-sizeof before upsert. Returns HTTP 413 if exceeded.upsert on the composite unique constraint (projectId, key) — a put either inserts a new entry or overwrites the existing value for that key in the project.store-entry
| Column | Type | Notes |
|---|---|---|
| id | string | BaseColumnSchemaPart |
| created | timestamp | BaseColumnSchemaPart |
| updated | timestamp | BaseColumnSchemaPart |
| key | string | Max length 128 (STORE_KEY_MAX_LENGTH) |
| projectId | string | ApIdSchema |
| value | jsonb (nullable) | Any JSON value |
Unique constraint: (projectId, key) — one value per key per project.
All routes are prefixed /v1/store-entries. All require securityAccess.engine() — only the flow engine (worker) can call these.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | / | engine | Upsert a key-value entry (create or overwrite) |
| GET | / | engine | Get entry by key; returns 404 if not found |
| DELETE | / | engine | Delete entry by key (no-op if not found) |
The projectId is taken from request.principal.projectId (set by the engine token), not from the request body — callers only provide key and value.
storeEntryService (simple service pattern, no logger parameter)
upsert({ projectId, request }) — sanitizes the value, upserts by (projectId, key), returns the resulting StoreEntry.getOne({ projectId, key }) — returns StoreEntry | null. Controller returns 404 if null.delete({ projectId, key }) — deletes the entry. Silent no-op if key does not exist.PutStoreEntryRequest: { key: string (max 128), value?: any }GetStoreEntryRequest: { key: string } (query param)DeleteStoreEntryRequest: { key: string } (query param)sizeof(request.body.value) > STORE_VALUE_MAX_SIZE) happens in the controller before calling the service, so oversized values never reach the DB.value column is nullable — a piece can explicitly store null or undefined as the value for a key.projectId obtained from the engine principal, satisfying multi-tenant isolation without any explicit join or filter in the request.