HeadlessOps
Integrations

Data Stores

Persist state between integration runs with managed data stores

Data Stores

Data Stores are managed tables for persisting state between runs. They support typed columns and are accessed from step code via ctx.stores.

Column Types

TypeDescription
TEXTString value
NUMBERNumeric value (returned as string — convert with parseInt() or parseFloat())
BOOLEANBoolean value
DATEISO 8601 date/datetime string
JSONArbitrary JSON object

Create a Store

curl -X POST https://app.headlessops.ai/api/stores \
  -H "Authorization: Bearer iter_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "processed-invoices",
    "description": "Tracks invoices already processed by the billing pipeline",
    "columns": [
      { "name": "invoice_id", "type": "TEXT", "required": true },
      { "name": "processed_at", "type": "DATE", "required": false },
      { "name": "amount", "type": "NUMBER", "required": false }
    ]
  }'

Using Stores in Steps

Access stores via ctx.stores.STORE_NAME. Underscores in attribute names are automatically converted to hyphens for slug lookup:

// ctx.stores.processed_invoices → looks up slug "processed-invoices"
const store = ctx.stores.processed_invoices;

Insert a Row

const row = await ctx.stores.processed_invoices.insert({
    invoice_id: "inv_123",
    processed_at: "2026-01-15T10:00:00Z",
    amount: 4999,
});
ctx.log("inserted", { id: row.id });

Find Records

// Find one — returns object or null
const record = await ctx.stores.processed_invoices.find_one({ invoice_id: "inv_123" });
if (record) {
    return { status: "already_processed" };
}

// Find many — returns array
const results = await ctx.stores.processed_invoices.find({ status: "pending" });

Update Records

const result = await ctx.stores.processed_invoices.update(
    { invoice_id: "inv_123" },
    { status: "sent" }
);
ctx.log("updated", { count: result.updated });

Delete Records

await ctx.stores.processed_invoices.delete({ invoice_id: "inv_123" });

Idempotency Pattern

Always check before inserting to make your automations safe to retry:

export async function run(input: Record<string, any>, ctx: StepContext) {
    const orderId = input.order_id;

    const existing = await ctx.stores.processed_orders.find_one({ order_id: orderId });
    if (existing) {
        ctx.log("already processed, skipping", { order_id: orderId });
        return { status: "skipped" };
    }

    // ... do the work ...

    await ctx.stores.processed_orders.insert({
        order_id: orderId,
        processed_at: "2026-01-15T10:00:00Z",
    });
    return { status: "success" };