Express CSV Logo

Prompted edits

Prompted edits let users change imported data from the review step with a prompt. They can clean values, normalize formats, add rows, update rows, or remove rows before the final import is delivered to your app.

You choose how prompted edits work:

  • Use managed prompted edits with { type: "managed" } when you want to run our standard prompt interpretation, code generation, and sandbox execution.
  • Use custom prompted edits with { type: "custom", edit } when you want to run your own prompt interpretation and code generation logic.

Prompted edits are disabled by default.

Managed Prompted Edits

For the hosted ExpressCSV AI path, pass { type: "managed" } to useExpressCSV().

import { useExpressCSV, x } from "@expresscsv/react";

const schema = x.row({
  sku: x.string().label("SKU"),
  name: x.string().label("Product name"),
  price: x.number().min(0).label("Price"),
});

export function ImportProductsButton() {
  const { open } = useExpressCSV({
    schema,
    getSessionToken: async () => fetchSessionToken(),
    importIdentifier: "product-import",
    promptedEdits: { type: "managed" },
  });

  return (
    <button
      onClick={() =>
        open({
          onData: async (_chunk, next) => next(),
        })
      }
    >
      Import products
    </button>
  );
}

Managed Usage Limits

Managed prompted edits are not available on the free plan. Paid plans include usage under ExpressCSV fair-use limits.

How Prompted Edits Run

Managed prompted edits generate JavaScript for the requested operation, execute it in a sandbox, and apply the result as proposed grid changes.

The managed path supports two execution scopes:

ScopeUse forNotes
rowUpdating or deleting existing rows, or adding rows based on existing row dataRuns once per row and can read the current row plus rowIndex and totalRows.
datasetAdding new rows without reading existing rowsRuns once and can only add rows.

If the prompt is ambiguous, impossible, or needs external data the importer does not have, the edit can return a no-op message instead of changing data.

Custom Prompted Edits

Pass type: "custom" when your own service should interpret prompts and return the proposed changes. The React SDK keeps your handler stable across renders and delegates edit requests through the importer iframe.

import {
  useExpressCSV,
  type PromptedEditsOptions,
} from "@expresscsv/react";

type CustomPromptedEdits = Extract<
  PromptedEditsOptions<typeof schema>,
  { type: "custom" }
>;

const edit: CustomPromptedEdits["edit"] = async ({
  sessionId,
  prompt,
  fields,
  rows,
  totalRows,
}) => {
  const response = await fetch("/api/ai/prompted-edits", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      sessionId,
      prompt,
      fields,
      rows,
      totalRows,
    }),
  });

  if (!response.ok) {
    throw new Error("Prompted edit failed");
  }

  return (await response.json()) as Awaited<ReturnType<typeof edit>>;
};

const { open } = useExpressCSV({
  schema,
  getSessionToken: async () => fetchSessionToken(),
  importIdentifier: "product-import",
  promptedEdits: {
    type: "custom",
    edit,
  },
});

Your handler receives:

PropertyDescription
sessionIdThe current import session ID.
promptThe user's natural language request.
fieldsEditable field keys, display names, types, and sample values.
rowsCurrent rows in display order, including rowId, rowIndex, and raw values for every editable field.
totalRowsTotal number of rows being edited.

Return type: "no-op" when the request should not change data:

return {
  type: "no-op",
  message: "Tell me which column should be used for the SKU prefix.",
};

Return type: "success" with rowId-based changes when you have changes:

return {
  type: "success",
  changes: [
    { type: "add", values: { sku: "NEW-001", name: "New product", price: "12.50" } },
    {
      type: "update",
      rowId: rows[0].rowId,
      values: {
        name: "Trimmed product name",
        price: "12.50",
      },
    },
    { type: "remove", rowId: rows[4].rowId },
  ],
};

update changes only the fields included in values; remove deletes rows by rowId; add creates new rows.

Use rowIndex only to interpret prompts that refer to the row's current display position. For example, a prompt like "update the first 10 rows" should select rows where rowIndex is less than 10, then return update changes using each selected row's rowId. Do not use rowIndex as the stable identifier in the returned changes, because sorting, filtering, or row insertion can change where a row appears.