---
title: "Upsert rows on matchColumns (the single v2 batch write primitive)"
method: POST
path: "/tables/{tableId}/rows/upsert"
tags: ["Workspace"]
---

# Upsert rows on matchColumns (the single v2 batch write primitive)

`POST /tables/{tableId}/rows/upsert`

v2's one row-write endpoint — there is no bare per-row insert (an
insert-only call is just an upsert whose rows match nothing). All writes
go through `api_batches` and are idempotent per `batchId`.

Rows matching an existing row on **every** `matchColumns` value update
that row's input cells; non-matching rows insert. Per-row
`inserted`/`updated`/`skipped` outcomes are recorded on the batch.

- Row keys and `matchColumns` are **input column slugs** (from
  `GET /tables/{tableId}/columns` → `columns[].slug`), never display
  names. A non-slug row key → `UNKNOWN_FIELDS`.
- `matchColumns` (required) must be real input columns (`UNKNOWN_COLUMN`)
  and every match value must be present + non-empty on every row
  (`MISSING_MATCH_VALUE`).
- Duplicate request keys → `DUPLICATE_MATCH_KEY`; multiple existing rows
  match one key → `AMBIGUOUS_MATCH` (409).
- `enrich` (default true) enriches freshly **inserted** rows.
- `reenrichUpdated` (default false) — set true to also re-run enrichment
  on rows the upsert **updated** (re-spends credits); off by default so an
  upsert never silently re-enriches matched rows.

Only `input`-kind columns are writable. Enrichment, score, and
**sequence** columns are populated automatically — sequences are
agent/column-generated only (see `POST /tables/{tableId}/campaigns`),
never set via this endpoint; passing one returns `NON_INPUT_COLUMNS`.

The response references the tracking object by id: it is an
`enrichment_run` the caller polls via `GET /enrichment-runs/{runId}`.

## Headers

- `Idempotency-Key` string

## Request body

- object
  - `rows` object[], required
  - `matchColumns` string[], required
  - `enrich` boolean
  - `reenrichUpdated` boolean
  - `batchId` string, uuid

## Response `200`

Idempotent replay of an existing `batchId` (same receipt shape).

- UpsertReceipt — Response of `POST /tables/{tableId}/rows/upsert` and `.../rows/upsert-file` — an `enrichment_run` reference the caller polls via `GET /enrichment-runs/{runId}`.
  - `object` 'enrichment_run', required
  - `id` string, uuid, required
  - `batchId` string, uuid, required — Legacy alias of `id`.
  - `counts` object, required
    - `inserted` integer, required
    - `updated` integer, required
    - `skipped` integer, required

## Other responses

- `201` — The upsert receipt — an `enrichment_run` reference with per-outcome counts.

---

[API](https://skmtc.net/origami/apis/origami-agent-api.md) · [All operations](https://skmtc.net/origami/apis/origami-agent-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/origami/origami-agent-api/versions/8083ad7fe081/schema)
