Blocks

Save to store

Upsert one keyed row into a workspace data store: same key replaces or merges, new key inserts, every write validated against the schema. Idempotent saves.

Write one keyed row into a workspace data store. Same key → the row is replaced (or merged, see Mode); new key → inserted. Every write is validated against the store's schema, and the row records which run wrote it.

What it does

The step renders the row key and every field value against the upstream payload, assembles them into one map, validates that map against the store's schema, and writes it under the key. The whole thing is an upsert: there is no separate insert and update, and no "already exists" error. Running the same workflow twice over the same data produces the same rows, not duplicates.

The store is resolved through the run's workspace, so a slug or id from another workspace comes back as not found. Each row keeps the id of the run and the workflow that last wrote it; the row browser in Settings links straight to that run.

Config

The save-to-store step in the Inspector Store, row key, and the field assignments forming the row.

  • Store: pick the store (created in Settings → data with a name and schema). The AI builder writes the store's slug here; both work.
  • Mode: replace (default) makes the row exactly the fields you set, dropping anything else that was there. merge deep-merges your fields into the existing row: fields you don't set survive, nested maps merge, lists and scalars you set replace.
  • Row key: template-rendered. Keys are your contract: a county FIPS, an email, an order id, whatever callers will look the row up by. A key that renders empty fails the step.
  • Row data: one row per field, a Field and a Value; values are templates, and a whole-string {{ … }} keeps its type, so numbers stay numbers and lists stay lists. Field names are dot-paths, so address.city nests.

Output

{
  "key": "41051",
  "data": { "fee_cents": 7600, "source_url": "https://…" },
  "mode": "replace",
  "store_slug": "county-fees",
  "updated_at": "2026-08-08T12:00:00Z"
}

data is the row as written. After a merge that is the full merged row, not just the fields this step set, so a downstream step can read {{ payload.data.fee_cents }} even when another step wrote it. key and store_slug ride along for logging.

Patterns

  • Durable results: extraction and enrichment pipelines write here; the data outlives the run.
  • Read it from anywhere: workflows via get from store; external systems over the read API with a developer token.
  • Idempotent pipelines: because writes are upserts, re-running a workflow overwrites rather than duplicates.
  • Accumulating state: a per-customer or per-thread row that several workflows touch takes merge mode, so the step that updates last_seen doesn't wipe notes.
  • Work queue: write rows with the expensive field left out, then let a second workflow list the rows still missing it and fill them in batches.

Gotchas

  • A write that doesn't match the store's schema fails the step with every violation listed. Fix the field mapping, don't loosen the schema. Fields marked sensitive in the schema are encrypted at rest and come back masked in Settings.
  • replace replaces the whole row, so include every field the row should keep, not just the one you're changing. Switch to merge when the row accumulates state across runs.
  • merge is read-then-write, not atomic. Two runs merging the same key at the same moment can drop the loser's fields, which is fine for per-thread state and wrong for counters.
  • Stores have a row cap (100,000 by default). A write that would push a store past its cap fails the step.

Related