Skip to main content

data-app-actions

Make a Metabase data app write data — a table action that inserts, updates or deletes a row of a published table, defined with `defineAction` in `src/metabase.actions.ts`, and the `useAction` hook from `@metabase/embedding-sdk-react/data-app` behind a form or a button, with inputs typed from each parameter, the server's error shown as it comes, and every affected query refreshed. Triggers — "a form that saves a row", "a button that updates a record", "delete an entry from the app", "submit doesn't refresh the list", "the action fails in the app". Reads are `data-app-data`.

Informations de source

Dépôt
metabase/metabase-cli
Dernière activité de la source
31 octobre 2026 à 01:02
Langue détectée de SKILL.md
anglais
Étoiles
17
Forks
7

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
data-app-actions
description
Make a Metabase data app write data — a table action that inserts, updates or deletes a row of a published table, defined with `defineAction` in `src/metabase.actions.ts`, and the `useAction` hook from `@metabase/embedding-sdk-react/data-app` behind a form or a button, with inputs typed from each parameter, the server's error shown as it comes, and every affected query refreshed. Triggers — "a form that saves a row", "a button that updates a record", "delete an entry from the app", "submit doesn't refresh the list", "the action fails in the app". Reads are `data-app-data`.
allowed-tools
Read, Write, Edit, Bash, AskUserQuestion
requires
["dataApps"]
# Writes in a data app A data app writes through **table actions**: an insert, update or delete of one row of a table published to the Library, by its primary key. It defines each one in `src/metabase.actions.ts`, one export per action: ```ts import { defineAction } from "@metabase/embedding-sdk-react/data-app"; import { schema } from "./metabase.schema"; export const updateOrder = defineAction({ entityId: "J2mXq8sLb0vT5nYcW4kRd", kind: "row/update", table: schema.tables.orders, }); ``` - `kind` is `row/create`, `row/update` or `row/delete`; `table` is a table of `schema` (`data-app-data`). The table needs exactly one primary key, and its database needs data actions turned on (_Data actions on the database_): settle both before writing the export. Show the user the table and kind and add it once they agree; a request that already names the write ("let me add a note") is that agreement. - `entityId` comes from `mb nano-id`, never invented; keep it when editing the export. The export's name is the action's name in Metabase. - There are no query actions and no SQL: a write a table action can't do (a computed value, several rows, a row matched by another column) is out of reach for the app — say so. Never copy an action from a data actions collection. - To drop an action, delete its export. The dev preview runs the action without saving it; Metabase runs it once the app is released (`data-app`, _Release_). ## Data actions on the database Before the first action on a database, read its toggle: `mb db get <db-id> --full --json --profile <name> | jq '.settings["database-enable-actions"]'` (`<db-id>` from `mb db list --profile <name> --json`, or `mb table get <table-id> --json --profile <name> | jq .db_id` once `src/metabase.schema.ts` gives the table's `id`). Anything but `true`, an unset `null` included, means it's off, and every action on that database fails with `Actions are not enabled.`, in the preview and in Metabase. Then ask with `AskUserQuestion` ("Data actions are off on <database>. Turning them on lets anyone with access to its actions write to it."), offering: - **Turn them on now**: run `mb db set-data-actions <db-id> true --profile <name>`, then read the toggle again. - **I'll turn them on**: the user does it in Admin → Databases → the database ("Allow data actions that use this database to be run"); wait for them, then read the toggle again before going on. - **No writes**: build the app without actions, and tell the user which features that leaves out. Never turn the toggle on without that yes. ## The hook ```tsx import { useAction } from "@metabase/embedding-sdk-react/data-app"; import { updateOrder } from "../metabase.actions"; const { execute, isExecuting, result, error, reset } = useAction(updateOrder); ``` - **Import it from `/data-app`.** The main `@metabase/embedding-sdk-react` entry exports the general SDK `useAction`, which takes raw ids and isn't typed from the definition. Write no generics: the definition types `execute`'s keys and values. - `execute(params)` resolves to the response body; on failure it both throws and sets `error`. It resolves to `null` without sending anything while the SDK is still starting, so guard a call that can happen at mount. It never runs on its own and has no `enabled`: return early in the handler to skip it. - `isExecuting` is true until it settles: disable the trigger and show progress, so a double click can't send twice. - `result` is the last response, `null` before the first call: `{ "created-row": <the new row> }` for an insert, `{ "rows-updated": <n> }` for an update, `{ "rows-deleted": <n> }` for a delete. `reset()` clears `result` and `error`, for example when the user dismisses a message. ## A form ```tsx export function NoteForm({ orderId, onSaved }: { orderId: number; onSaved: () => Promise<void> }) { const { execute, isExecuting, error } = useAction(updateOrder); const [note, setNote] = useState(""); const [isValid, setIsValid] = useState(false); async function onSubmit(event: FormEvent<HTMLFormElement>) { event.preventDefault(); try { await execute({ id: orderId, note }); // keys are column names, in lowercase await onSaved(); // the parent refetches what the write changed setNote(""); setIsValid(false); } catch { // shown from `error` } } return ( <form onSubmit={onSubmit} onChange={(event) => setIsValid(event.currentTarget.checkValidity())}> <input type="text" required value={note} onChange={(event) => setNote(event.target.value)} /> <button type="submit" disabled={isExecuting || !isValid}> {isExecuting ? "Saving…" : "Save"} </button> {error ? ( <pre style={{ whiteSpace: "pre-wrap", margin: 0 }}> {error.data.message ?? "Action failed."} </pre> ) : null} </form> ); } ``` **Parameters.** The keys `execute` takes are the table's column names in lowercase (`CREATED_AT` becomes `created_at`; names already in lowercase stay as they are), each typed from its field's `jsType` in `src/metabase.schema.ts`, and all optional in the type. Pick each input from `jsType`, never from the name — a column called `email` with `jsType: "string"` is still `type="text"`. | `jsType` | Input | | --------- | ---------------------------------------------------------- | | `string` | `type="text"`; a `<textarea>` for long text such as a note | | `number` | `type="number"`, read with `valueAsNumber` | | `boolean` | a checkbox or switch | | `Date` | `type="date"`, or `datetime-local` when a time is expected | | `unknown` | `type="text"`, converted at the call | - **Insert** (`row/create`): every column but an auto-increment key and the columns the database fills when the form leaves them out, such as a `created_at` or `status` with a default. `mb table get <id> --include fields --full --json` gives each field's `database_required` and `database_default`. `result["created-row"]` holds the new row, its key among them. - **Update** (`row/update`): the key and the columns to change. An edit form starts from the row's current values, read by the query on screen, and sends them all; a button that sets one column (mark as done) sends only the key and that column. A fixed value such as `"done"` comes from the column's values (`mb field values <field-id> --profile <name>`), never a guess. A `null` is dropped, so an update can't clear a column. - **Delete** (`row/delete`): the key only. Ask the viewer to confirm first. **Validation** is `required` on the key of an update or delete and on each `NOT NULL` column without a default in an insert, and nothing else — no length, range or format checks of your own. Disable submit from the browser's verdict, `form.checkValidity()`, never from hand-rolled checks. Anything stricter comes back from the server. **Errors.** `error` is `{ status?, data: { message?, errors? }, isCancelled }` — read it without a cast. Refusals and constraint errors are 400s whose `data.message` carries the server's text (`Sorry, the row you're trying to update doesn't exist`): show it as it comes, in a `white-space: pre-wrap` element, since driver messages span lines — never "Something went wrong", `String(error)` or a friendlier paraphrase. `error.data.errors?.[parameter]`, when present, goes beside its field (`aria-invalid` on the input). ## Refresh After a write succeeds, refetch every query on screen it could have changed: `await Promise.all([orders.refetch(), totals.refetch()])`, the `refetch` that `useMetabaseQuery` returns. - Those queries live in a component above the trigger, which passes the refresh down; a query in the trigger's own component or below it can't be wired to it. - A `StaticQuestion` panel has no `refetch`: give it a `key` the parent bumps after the write, which remounts it and runs the question again. - Await the refresh before clearing the form or closing the modal, so stale rows never flash. - Never prepend `result` to a local list instead: ids, timestamps and computed columns come from the server. ## When it doesn't work 1. Log `result`, `error` and the object passed to `execute`: every key is a lowercase column name, every value matches its `jsType`. 2. `execute` resolved to `null` and nothing was sent: it ran before the SDK started. 3. The screen doesn't update: see _Refresh_. 4. It works in the preview but not in Metabase: the app was released without running `mb data-app generate-actions` (`data-app`, _Release_). Then release the app (`data-app`, _Release_).
Voir sur GitHub