Skip to main content

Stores

A Store is a named data persistence target. Stores allow you to save, retrieve, update, and query data during mission execution.

Basic syntax

store storeName: adapter("identifier")

Available adapters

AdapterDescriptionUse Case
memoryIn-memory storageTesting, temporary data
fileJSON file storageLocal development, small datasets
postgrestPostgREST-backed tablePostgres via a PostgREST endpoint
sqlSQL store (PostgREST under the hood)See SQL store below
nosqlNoSQL placeholderNot yet implemented

Memory store

In-memory storage that doesn't persist between runs:

store cache: memory("cache")
store tempData: memory("temp")

Best for:

  • Testing
  • Temporary processing data
  • Intermediate results

File store

JSON file storage in the .reqon-data directory:

store customers: file("customers")
store orders: file("orders")

Creates files like:

.reqon-data/
├── customers.json
└── orders.json

Best for:

  • Local development
  • Small to medium datasets
  • Simple persistence without database setup

PostgREST store

Persist to a Postgres table through a PostgREST endpoint:

store customers: postgrest("customers")
store orders: postgrest("orders")

This is the production-ready database adapter. See PostgREST Store for connection options.

SQL and NoSQL stores

sql and nosql are placeholders rather than full database adapters. There's no MongoDB or DynamoDB support.

store customers: sql("customers_table")
store events: nosql("events")

In --dev mode, both fall back to local JSON files. Outside dev mode, a sql store needs PostgREST configuration (use postgrest directly), and nosql errors loudly because it isn't implemented. For real database persistence, use the postgrest adapter.

Store operations

Writing data

action SaveData {
get "/users"

// Store entire response
store response -> users

// Store with key for indexed access
store response -> users { key: .id }

// Store nested data
store response.data.items -> items { key: .itemId }
}

Key option

The key option specifies which field to use as the unique identifier:

store response -> users { key: .id }
store response -> users { key: .email }
store response -> users { key: .orgId + "-" + .userId }

Upsert mode

Insert or update based on key:

store response -> users {
key: .id,
upsert: true
}

Partial updates

Update only provided fields:

store response -> users {
key: .id,
partial: true
}

Reading from stores

Stores are available as variables in actions:

action ProcessStoredData {
// Iterate over store contents
for user in users {
// Access user data
get "/orders?userId=" + user.id
}

// With filtering
for user in users where .status == "active" {
// Process active users only
}
}

Store interface

Stores implement this interface:

interface StoreAdapter {
// Get single record by key
get(key: string): Promise<Record | null>

// Set record with key
set(key: string, value: Record): Promise<void>

// Update record (partial)
update(key: string, partial: Record): Promise<void>

// Delete record
delete(key: string): Promise<void>

// List all records with optional filter
list(filter?: FilterOptions): Promise<Record[]>

// Clear all records
clear(): Promise<void>
}

Filtering store data

Use where clauses when iterating:

action ProcessFiltered {
// Status filter
for order in orders where .status == "pending" {
// Process pending orders
}

// Multiple conditions
for user in users where .active == true and .role == "admin" {
// Process active admins
}

// Comparison
for product in products where .price > 100 {
// Process expensive products
}
}

Checking store contents

validate takes a target expression, and match arms route on schema name or _:

action CheckStore {
// Fail the mission if the store is empty
validate users {
assume length(users) > 0
}
}

Cross-store operations

Reference multiple stores:

action Reconcile {
for order in orders {
// Look up related customer
for customer in customers where .id == order.customerId {
map order -> EnrichedOrder {
id: order.id,
amount: order.amount,
customerName: customer.name,
customerEmail: customer.email
}
store order -> enrichedOrders { key: .id }
}
}
}

Best practices

Use appropriate adapters

// Development
store data: file("data")

// Production
store data: postgrest("data_table")

Always specify keys

// Good: explicit key
store response -> users { key: .id }

// Avoid: no key. Without one, the store falls back to each record's
// `id` field and throws if that's missing - nothing is auto-generated.
store response -> users

Use upsert for sync operations

action IncrementalSync {
get "/users" { since: lastSync }

for user in response {
store user -> users { key: .id, upsert: true }
}
}

Clean up temporary stores

mission CleanPipeline {
store temp: memory("temp")

action Process {
// Use temp store
}

// Temp store is automatically cleaned when mission ends
}

Use descriptive store names

// Good
store activeCustomers: file("active-customers")
store pendingOrders: file("pending-orders")
store syncedInvoices: file("synced-invoices")

// Avoid
store data: file("data")
store temp: file("temp")

Exporting store data

Use the CLI to export stores after execution. --output writes a single combined JSON file keyed by store name:

reqon mission.vague --output ./export.json

Or programmatically:

const result = await execute(source);

for (const [name, store] of result.stores) {
const data = await store.list();
fs.writeFileSync(`${name}.json`, JSON.stringify(data, null, 2));
}