metabase-data-app-actions
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTriggering actions from a Metabase data app
在Metabase数据应用中触发动作
A Metabase action is a server-defined write operation against the data warehouse — either a basic CRUD operation (insert / update / delete) on a model, or a custom SQL command. Actions are configured ahead of time on the Metabase instance, with their parameters, model bindings, and permissions already set. A data app's job is to invoke an action with the right parameters when the user does something — clicking a button, submitting a form, confirming a destructive prompt.
Metabase 动作(action) 是在服务器端定义的、针对数据仓库的写入操作——可以是针对模型的基础CRUD操作(新增/更新/删除),也可以是自定义SQL命令。动作需要提前在Metabase实例中配置完成,其参数、模型绑定关系和权限都已预先设置。数据应用的职责是在用户执行操作时(点击按钮、提交表单、确认危险操作提示等),调用对应动作并传入正确的参数。
The mental model
心智模型
Actions belong to a model. They mutate that model's rows. Concretely:
- Every action has a parent model. In the schema it appears as .
schema.models.<modelName>.actions.<actionName> - Model entries are included here only to catalog actions. Do not render models as questions, pass model ids to , or fetch model rows; use semantic-layer queries/questions for read views.
InteractiveQuestion - An action's is either
type(CRUD on the model) or"implicit"(custom SQL the user authored)."query" - Implicit actions have an that says what they do:
implicitKind,"row/create","row/update", or"row/delete"variants."bulk/*" - Each action publishes a list. Each parameter has a
parameters(the key the Data App sends), aslug(jsType/"string"/"number"/"Date"/"boolean"), and an optional"unknown"flag.required - Use to know which fields to render and submit. A create result may include
action.parameters, but that row is only typed asresult["created-row"]; use it for lightweight confirmation, then refresh the existing table/question/query data already used by the page. Do not fetch or render the parent model itself.Record<string, RowValue>
动作隶属于某个模型,用于修改该模型的行数据。具体来说:
- 每个动作都有一个父模型。在schema中,它的路径为。
schema.models.<modelName>.actions.<actionName> - 此处包含模型条目仅用于对动作进行分类。不要将模型渲染为问题(question)、不要将模型ID传递给,也不要直接获取模型行数据;读取视图请使用语义层查询/问题。
InteractiveQuestion - 动作的分为
type(模型的CRUD操作)和"implicit"(用户编写的自定义SQL)两种。"query" - 隐式动作(implicit action)有一个字段用于说明操作类型:
implicitKind、"row/create"、"row/update",以及"row/delete"类的批量操作变体。"bulk/*" - 每个动作都会对外暴露一个列表。每个参数包含
parameters(数据应用发送时使用的键名)、slug(jsType/"string"/"number"/"Date"/"boolean"),以及可选的"unknown"标识。required - 请通过来确定需要渲染和提交哪些字段。创建操作的结果可能包含
action.parameters,但该行的类型仅为result["created-row"];可将其用于轻量级的确认提示,随后刷新页面已在使用的表格/问题/查询数据。不要直接获取或渲染父模型本身。Record<string, RowValue>
What's in the schema (and what isn't)
schema包含(及不包含)的内容
Action entries are only generated when the typed schema includes models. Before writing action-invoking code, make sure the schema was generated with ; with , Metabase includes models/actions for that database only. Do not rely on for actions; question collections only add saved questions.
include-models=truedatabase=<name-or-id>&include-models=truequestion-collectionsBefore writing any action-invoking code, look at the schema and enumerate what's available under across the models the app cares about. The schema is your complete catalog of actions for the instance, not a catalog of model data to display. If a model's entry has , , , those are the actions invokable. Anything not present doesn't exist as far as the Data App is concerned.
schema.models.<m>.actionsactionscreateupdatedelete只有当类型化schema包含模型时,才会生成动作条目。在编写动作调用代码之前,请确保schema是通过参数生成的;如果使用,Metabase只会包含该数据库的模型/动作。不要依赖来获取动作;问题集合仅包含已保存的问题。
include-models=truedatabase=<name-or-id>&include-models=truequestion-collections在编写任何动作调用代码之前,请查看schema,枚举应用所关心的所有模型下中可用的动作。schema是实例中动作的完整目录,而不是用于展示的模型数据目录。如果某个模型的条目包含、、,这些就是可调用的动作。对于数据应用来说,任何未出现在schema中的动作都视为不存在。
schema.models.<m>.actionsactionscreateupdatedeleteThe hook
Hook介绍
ts
import { useAction } from "@metabase/embedding-sdk-react/data-app";
const { execute, isExecuting, result, error, reset } = useAction(MyAction);- Import it from . That entry's
@metabase/embedding-sdk-react/data-appis the data-app form: it accepts auseActionexport ordefineAction(...)and nothing else. The main entry'snullis the general SDK hook, which also takes plain objects and raw ids, so it cannot enforce the definition; a data app that imports it from the main entry compiles with an unsynchronized action and fails in production.useAction - The argument is the export itself, declared in the app's root-level
defineAction(...)directory (oneactions/file per topic, beside<topic>.action.ts, never underpackage.json) — passsrc/, notMyAction. The hook accepts nothing else in a data app: an inlineMyAction.copiedActionIdobject, the schema entry, or a spread copy of a definition fails to compile with{ action: ... }. Fix that by adding the export toProperty 'definedWithDefineAction' is missing, not with a cast and not by callingactions/at the hook, which compiles but is never synchronized. A production build then runs the synchronized copy, whose model sits in the app's own collection; the dev preview keeps running the authored action, so an app works before it has ever been synchronized. Do not passdefineAction(...)or itsschema.models.<model>.actions.<action>: the entry is a compile error, and the raw id belongs to the original model, which the app's users cannot read, so the call fails on permissions in production..id - Don't write the generics. The definition carries its schema entry, so the hook infers both the parameters object and the discriminated from it:
resultbecomes a keyed object (parameters[]entries are required keys, each value typed from itsrequired: true), andjsType/implicitKindbecome the kind (type→"row/create","create"→"row/update","update"→"row/delete", any"delete"→"bulk/*","bulk"→type === "query"). The raw-id form exists only on the SDK's"sql"from the main entry, whereuseActionneeds them spelled out since an id describes nothing; a data app never names an action by id.useAction<TParameters, TKind>(42) - — triggers the action. Parameters object is keyed by parameter
execute(parameters); parameters declaredslugare required keys, everything else optional. Returns the response body on success AND throws on failure (the error is also written torequired: truestate for render-time consumers). Resolves toerror(without making a request) whennullisactionIdor the SDK is not yet initialized — guard the call site if those cases are reachable.null - No /
enabledargument. The hook only ever runs whenoptionsis called, so a gate option would be redundant. Skip the action by branching in the event handler:execute(...)tsconst onClick = async () => { if (!user.canEdit) return; await execute({ id: orderId, discount }); }; - —
isExecutingbetween the call and its resolution. Drive buttontruefrom this so the user can't double-click into duplicate requests.disabled - — the response body, discriminated by
result(or theTKindunion whenAnyActionResultis omitted).TKindbefore the first call and afternull. Use it for lightweight confirmation (reset()after an insert,result?.["created-row"]after a SQL action), but do not treat create rows as richly typed model rows; refresh surrounding data instead — see After an action runs.result?.["rows-affected"] - — the last thrown error, typed
error. Read fields directly with no cast:ActionExecuteError | null,error?.data?.message,error?.status.error?.isCancelled - — clears
reset()andresultback toerror. Useful after the user acknowledges success or dismisses an error.null
The hook does NOT auto-fire on mount. Actions only run when the Data App calls explicitly.
execute(...)ts
import { useAction } from "@metabase/embedding-sdk-react/data-app";
const { execute, isExecuting, result, error, reset } = useAction(MyAction);- 请从导入。 该入口导出的
@metabase/embedding-sdk-react/data-app是数据应用专用版本:它仅接受useAction的导出结果或defineAction(...),不接受其他参数。主入口的null是通用SDK hook,还可以接受普通对象和原始ID,因此无法对动作定义进行校验;从主入口导入useAction的数据应用在编译时会使用未同步的动作,在生产环境中会运行失败。useAction - 入参是的导出结果本身,该定义需声明在应用根目录的
defineAction(...)文件夹下(每个主题对应一个actions/文件,放在<topic>.action.ts同级目录,绝不能放在package.json下)——请传递src/,而不是MyAction。在数据应用中,该hook不接受其他任何形式的参数:内联的MyAction.copiedActionId对象、schema条目、或者定义的展开拷贝都会编译失败,报错{ action: ... }。请通过将导出添加到Property 'definedWithDefineAction' is missing目录来解决此问题,不要使用类型断言,也不要在hook调用处直接调用actions/——后者虽然能通过编译,但永远不会被同步。生产构建会运行同步后的动作副本,其模型位于应用自己的集合中;而开发预览会继续运行原始编写的动作,因此应用在同步之前就可以正常工作。切勿传递defineAction(...)或它的schema.models.<model>.actions.<action>:前者会导致编译错误,而原始ID属于原模型,应用的用户没有读取权限,因此在生产环境中调用会因权限不足失败。.id - 不要手动编写泛型。 动作定义本身携带了对应的schema条目,因此hook可以自动推断参数对象和可区分的类型:
result会转换为带键的对象(parameters[]的条目为必填键,每个值的类型由其required: true决定),jsType/implicitKind会转换为操作类型(type→"row/create"、"create"→"row/update"、"update"→"row/delete"、任何"delete"→"bulk/*"、"bulk"→type === "query")。原始ID形式的调用仅存在于SDK主入口的"sql"中,在那里useAction需要手动指定泛型,因为ID本身不包含任何类型信息;数据应用永远不会通过ID来引用动作。useAction<TParameters, TKind>(42) - —— 触发动作。参数对象以参数的
execute(parameters)作为键名;声明为slug的参数是必填键,其余为可选。调用成功时返回响应体,失败时会抛出错误(错误同时会被写入required: true状态,供渲染时使用)。当error为actionId或SDK尚未初始化时,该方法会解析为null(不会发起请求)——如果可能出现这些情况,请在调用处做好防护。null - 没有/
enabled参数。 该hook仅在调用options时才会运行,因此开关类参数是多余的。请在事件处理函数中通过条件判断来跳过动作执行:execute(...)tsconst onClick = async () => { if (!user.canEdit) return; await execute({ id: orderId, discount }); }; - —— 在调用发起和结果返回之间为
isExecuting。请用这个值来控制按钮的true状态,避免用户双击导致重复请求。disabled - —— 响应体,类型由
result区分(如果省略TKind则为TKind联合类型)。首次调用前和调用AnyActionResult后值为reset()。可将其用于轻量级确认提示(比如插入操作后读取null,SQL动作后读取result?.["created-row"]),但不要将创建的行视为完整类型的模型行;请刷新周边的数据——参见动作执行后。result?.["rows-affected"] - —— 最近一次抛出的错误,类型为
error。可直接读取其字段,无需类型断言:ActionExecuteError | null、error?.data?.message、error?.status。error?.isCancelled - —— 将
reset()和result清空为error。在用户确认成功或关闭错误提示后非常有用。null
该Hook不会在组件挂载时自动执行。 只有当数据应用显式调用时,动作才会运行。
execute(...)Canonical usage — a form that creates a row
典型用法——创建行的表单
tsx
import { useAction } from "@metabase/embedding-sdk-react/data-app";
import { CreatePerson } from "../../actions/people.action";
function AddPersonForm({ onCreated }: { onCreated: () => void }) {
const { useState } = React;
const { execute, isExecuting, error, reset } = useAction(CreatePerson);
const [name, setName] = useState("");
const [email, setEmail] = useState("");
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
try {
await execute({ name, email }); // typed: keys match parameter slugs
setName("");
setEmail("");
onCreated(); // ← let the parent refresh dependent data
} catch {
// error is captured into hook state for render-time display
}
}
return (
<form onSubmit={onSubmit}>
<input value={name} onChange={(e) => setName(e.target.value)} />
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<button type="submit" disabled={isExecuting || !name || !email}>
{isExecuting ? "Saving…" : "Add"}
</button>
</form>
);
}tsx
import { useAction } from "@metabase/embedding-sdk-react/data-app";
import { CreatePerson } from "../../actions/people.action";
function AddPersonForm({ onCreated }: { onCreated: () => void }) {
const { useState } = React;
const { execute, isExecuting, error, reset } = useAction(CreatePerson);
const [name, setName] = useState("");
const [email, setEmail] = useState("");
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
try {
await execute({ name, email }); // typed: keys match parameter slugs
setName("");
setEmail("");
onCreated(); // ← let the parent refresh dependent data
} catch {
// error is captured into hook state for render-time display
}
}
return (
<form onSubmit={onSubmit}>
<input value={name} onChange={(e) => setName(e.target.value)} />
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<button type="submit" disabled={isExecuting || !name || !email}>
{isExecuting ? "Saving…" : "Add"}
</button>
</form>
);
}Showing the error message
显示错误信息
When fails, surface both error layers. The hook's is typed — shape . is the whole-request failure. is a per-field validation map keyed by parameter slug (), or for whole-request failures. Read both directly, no cast:
execute(...)errorActionExecuteError | null{ status?, data: { message?, errors? }, isCancelled }error.data.messageerror.data.errors{ <slug>: <message> }{}tsx
const fieldErrors = error?.data.errors ?? {};
{error ? (
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>
{error.data.message ?? "Action failed."}
</pre>
) : null}
{action.parameters.map((parameter) => {
const fieldError = fieldErrors[parameter.slug];
return (
<label key={parameter.slug}>
{parameter.displayName}
<input
aria-invalid={Boolean(fieldError)}
style={{ borderColor: fieldError ? "#dc2626" : undefined }}
/>
{fieldError ? <div role="alert">{fieldError}</div> : null}
</label>
);
})}Use (or any element with ) — the messages contain newlines that matter (driver errors include the SQL on its own line). A collapses them into one wall of text.
<pre>white-space: pre-wrap<span>Example: submitting a 9-character value into a column produces
CHARACTER(2)Value too long for column "STATE CHARACTER(2)": "'dadasdasd' (9)";
SQL statement:
UPDATE "PUBLIC"."PEOPLE" SET … WHERE "PUBLIC"."PEOPLE"."ID" = 1 [22001-214]That whole string is . Render it as-is.
error.data.messageDon't:
- Render /
"Failed"/"Something went wrong"instead ofString(error)— the user loses the only fix-it info they have.error.data.message - Render only for validation failures —
error.data.messageis often the actionable "which field and why" detail.error.data.errors[parameter.slug] - Paraphrase the message into a "friendlier" version. The raw H2/Postgres/SQL error is more actionable than any rewrite.
当失败时,请同时展示两层错误信息。该hook的类型为——结构为。是整个请求的错误信息。是按参数为键的字段级校验错误映射(),如果是全局请求错误则为。请直接读取这两个字段,无需类型断言:
execute(...)errorActionExecuteError | null{ status?, data: { message?, errors? }, isCancelled }error.data.messageerror.data.errorsslug{ <slug>: <message> }{}tsx
const fieldErrors = error?.data.errors ?? {};
{error ? (
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>
{error.data.message ?? "Action failed."}
</pre>
) : null}
{action.parameters.map((parameter) => {
const fieldError = fieldErrors[parameter.slug];
return (
<label key={parameter.slug}>
{parameter.displayName}
<input
aria-invalid={Boolean(fieldError)}
style={{ borderColor: fieldError ? "#dc2626" : undefined }}
/>
{fieldError ? <div role="alert">{fieldError}</div> : null}
</label>
);
})}请使用(或任何带有样式的元素)——错误信息中包含重要的换行符(驱动错误会将SQL单独占一行)。使用会将换行折叠成一整段文本,难以阅读。
<pre>white-space: pre-wrap<span>示例:向类型的列提交一个9字符的值会产生如下错误:
CHARACTER(2)Value too long for column "STATE CHARACTER(2)": "'dadasdasd' (9)";
SQL statement:
UPDATE "PUBLIC"."PEOPLE" SET … WHERE "PUBLIC"."PEOPLE"."ID" = 1 [22001-214]这整段字符串就是的内容,请原样渲染。
error.data.message不要做以下操作:
- 不要渲染/
"Failed"/"Something went wrong"来代替String(error)——用户会因此失去唯一能帮助他们解决问题的信息。error.data.message - 校验失败时不要只渲染——
error.data.message通常包含更具可操作性的「哪个字段、为什么出错」的细节。error.data.errors[parameter.slug] - 不要将错误信息改写成「更友好」的版本。原始的H2/Postgres/SQL错误比任何改写都更具可操作性。
After an action runs — keeping the UI honest
动作执行后——保持UI数据准确
When an action succeeds, the data the user sees may be stale. Without an explicit refresh, the list still shows the old rows; the stat tile still shows the old count; the row the user just edited still shows its old values. The action worked, but the UI lies — and there is no warning.
The rule is simple and absolute: after an action resolves successfully, every piece of data on the screen that the action could have changed must be refreshed.
当动作执行成功后,用户看到的数据可能已经过时。如果没有显式刷新,列表仍然显示旧的行数据;统计卡片仍然显示旧的计数;用户刚刚编辑的行仍然显示旧的值。动作本身执行成功了,但UI展示的是错误信息——而且没有任何警告。
这条规则简单而绝对:动作成功执行后,屏幕上所有可能被该动作修改的数据都必须刷新。
Mapping parameters
参数映射
Each entry on a schema action exposes , , , and optionally . The Data App's job:
parameters[]slugdisplayNamejsTyperequired-
Object keys inmatch the parameter
execute({ … })strings. Use them as literal string keys — the shape the hook derives from the definition rejects typos at compile time, but only when the argument is theslugexport (not a bare id).defineAction(...) -
Pick the right inputfrom
typeso the browser provides the correct UX and built-in coercion. The agent never invents the input type from the slug name (a slug calledjsTypeis still"phone"→jsType: "string").type="text"jsTypeInput element "string"<input type="text" …>"number"<input type="number" …>"boolean"(or a Mantine<input type="checkbox" …>/Switch)Checkbox"Date"(or<input type="date" …>if a time component is expected)"datetime-local""unknown"— best effort, coerce at the call site<input type="text" …> -
Value types inmatch
execute({ … }). AjsTypeparameter wants ajsType: "number", not a string. Withnumberthe input emits a number fortype="number", but.valueAsNumberis still a string — coerce at the call site (<input>.value) if you're reading the latter.Number(input) -
parameters cannot be omitted. Reflected in the TS type: required keys are required.
required: true -
is for labels; never use it as a key.
displayName
For implicit actions, the slugs match the model's column names (slugified). For custom SQL actions, slugs are whatever the action's author named the SQL parameters in Metabase. Either way, the schema is the source of truth.
schema中动作的每个条目都会暴露、、,以及可选的。数据应用需要做的是:
parameters[]slugdisplayNamejsTyperequired-
中的对象键名必须与参数的
execute({ … })字符串完全匹配。 请将它们作为字面字符串键使用——只有当入参是slug的导出结果(而不是裸ID)时,hook从定义中推导的类型才能在编译时捕获拼写错误。defineAction(...) -
根据选择正确的输入框
jsType,这样浏览器才能提供正确的用户体验和内置的类型转换。不要根据slug的名称来猜测输入类型(比如名为type的slug,其"phone"仍然是jsType→ 对应"string")。type="text"jsType对应输入元素 "string"<input type="text" …>"number"<input type="number" …>"boolean"(或Mantine的<input type="checkbox" …>/Switch)Checkbox"Date"(如果需要时间组件则用<input type="date" …>)"datetime-local""unknown"—— 尽力而为,在调用处进行类型转换<input type="text" …> -
中的值类型必须与
execute({ … })匹配。jsType的参数需要传入jsType: "number"类型,而不是字符串。当使用number时,输入框的type="number"会返回数字类型,但.valueAsNumber仍然是字符串——如果你读取的是后者,请在调用处进行转换(<input>.value)。Number(input) -
的参数不能省略。 这一点会反映在TypeScript类型中:必填键是必须传入的。
required: true -
用于标签显示;绝不要将其作为键名使用。
displayName
对于隐式动作,slug与模型的列名(经过slug化处理后)一致。对于自定义SQL动作,slug是动作的作者在Metabase中为SQL参数指定的名称。无论哪种情况,schema都是唯一的可信来源。
Form-side validation — match the main app
表单端验证——与主应用保持一致
The schema exposes (and for type coercion). That's the full validation contract today, matching Metabase's built-in action-execute form. No length checks, no min/max, no format checks — anything more granular comes back as a BE error after submit (see Showing the error message).
parameter.requiredjsTypeSchema is the ONLY source of truth. Read and wire it — nothing else. Don't add because a field is named , don't cap a "name" at 100 chars on a hunch, don't set from the slug. If the schema is silent, the field is unconstrained.
parameter.requiredrequired"email"type="email"Disable the submit button while the form is invalid. Drive it from the browser's native verdict so is the only rule in play:
requiredtsx
const [isFormValid, setIsFormValid] = useState(false);
const onFormChange = (e: React.FormEvent<HTMLFormElement>) =>
setIsFormValid(e.currentTarget.checkValidity());
<form onSubmit={...} onChange={onFormChange}>
<input required={parameter.required} />
<button type="submit" disabled={isExecuting || !isFormValid}>Create</button>
</form>No hand-rolled checks.
!name || !emailschema暴露了(以及用于类型转换的)。这是目前完整的验证约定,与Metabase内置的动作执行表单保持一致。没有长度检查、没有最小值/最大值限制、没有格式校验——任何更细粒度的校验都会在提交后以服务端错误的形式返回(参见显示错误信息)。
parameter.requiredjsTypeSchema是唯一的可信来源。 只需读取并进行绑定即可,不要额外添加规则。不要因为字段名为就添加必填校验,不要凭直觉将的长度限制为100字符,不要根据slug就设置。如果schema中没有说明,该字段就是无约束的。
parameter.required"email""name"type="email"表单无效时禁用提交按钮。 请使用浏览器原生的校验结果来控制,这样就能保证只有规则生效:
requiredtsx
const [isFormValid, setIsFormValid] = useState(false);
const onFormChange = (e: React.FormEvent<HTMLFormElement>) =>
setIsFormValid(e.currentTarget.checkValidity());
<form onSubmit={...} onChange={onFormChange}>
<input required={parameter.required} />
<button type="submit" disabled={isExecuting || !isFormValid}>Create</button>
</form>不要手动编写之类的校验逻辑。
!name || !emailDebugging Checklist
调试清单
When an action appears to succeed but the screen doesn't update, or a call fails with a 400:
- Log ,
result, anderrorafterisExecutingto confirm the request actually went through and succeeded.await execute(...) - Confirm was called with the
useActionexport imported fromdefineAction(...). Passing the schema entry, an inline object, or a spread copy is a compile error; passing itsactions/compiles but leaves.iduntyped and 403s in production, since the authored action is not the one an app's users can run. Callingexecuteinside the component compiles too, butdefineAction(...)scans onlysync-resources, so that action is never copied and also 403s.actions/ - Log the object passed to . Every key must match a parameter
execute({ ... })fromslug; every value must match its declaredschema.models.<model>.actions.<action>.parameters.jsType - List every data view on the screen that reads from the mutated model. Confirm each one's data hook is mounted ABOVE the action trigger so its refresh callback can be passed down.
- Confirm the refresh callback is called AFTER AND that the refresh itself is awaited. When multiple refreshes apply, confirm they're awaited together (
await execute(...)).Promise.all - For implicit actions, confirm the right matches what you intend (
implicitKindvsrow/createvsrow/update) — picking the wrong action publishes the wrong write.row/delete - If the schema doesn't list an action you expect, the action isn't defined on the instance or the schema file is stale. Regenerate the schema.
当动作看似执行成功但屏幕没有更新,或者调用返回400错误时,请按以下步骤排查:
- 在之后打印
await execute(...)、result和error,确认请求确实发出并成功执行。isExecuting - 确认的入参是从
useAction目录导入的actions/导出结果。传递schema条目、内联对象或展开拷贝会导致编译错误;传递它的defineAction(...)虽然能通过编译,但.id会失去类型保护,并且在生产环境中会返回403错误,因为用户可运行的不是原始编写的动作。在组件内部调用execute也能通过编译,但defineAction(...)只会扫描sync-resources目录,因此该动作永远不会被同步,同样会返回403错误。actions/ - 打印传递给的对象。每个键都必须与
execute({ ... })中的参数schema.models.<model>.actions.<action>.parameters匹配;每个值的类型都必须与声明的slug一致。jsType - 列出屏幕上所有从被修改的模型中读取数据的视图。确认每个视图的数据hook都挂载在动作触发组件的上方,这样它的刷新回调才能向下传递。
- 确认刷新回调是在之后调用的,并且刷新操作本身也被await了。如果有多个刷新操作,确认它们被同时await(使用
await execute(...))。Promise.all - 对于隐式动作,确认与你的预期一致(
implicitKind、row/create还是row/update)——选错动作会执行错误的写入操作。row/delete - 如果schema中没有列出你预期的动作,说明该动作未在实例上定义,或者schema文件已经过时。请重新生成schema。
Common Mistakes
常见错误
- Importing from
useActioninstead of@metabase/embedding-sdk-react. The main entry's hook is the general SDK one and accepts anything, so the definition check never runs.@metabase/embedding-sdk-react/data-app - Declaring the action anywhere but root-level : inline at the hook, wrapped in
actions/inside a component, or underdefineAction(...). Onlysrc/actions/is synchronized.actions/ - Passing to
schema.models.<model>.actions.<action>.idinstead of theuseActionexport.defineAction(...)then accepts anyexecute, typos likeRecord<string, unknown>slip through, and production 403s because the authored action is not the one an app's users can run.{ wrongKey: 1 } - Forgetting to refresh after a successful action. The UI keeps rendering stale data with no error or warning.
- Rendering /
"Failed"/"Something went wrong"instead of the real backend message. Always extractString(error)/error.data.message(the diagnostic the user needs is in there) and render it verbatim — see Showing the error message.error.data.errors - Calling the refresh callback without ing it. The modal dismisses or the form clears before fresh data arrives, leaving the user staring at the stale view for a beat.
await - Loading data INSIDE the same component that triggers the action. The hook is below the trigger, so its refresh callback can't be wired up. Lift the data hook to a parent and hand its refresh callback down to the trigger.
- Passing raw strings as parameter values when the slug's
<input>isjsTypeor"number". Coerce at the call site."boolean" - Letting the trigger fire while a previous request is in flight. Drive from the hook.
disabled={isExecuting} - Prepending the action's directly to a local list to skip the refresh. Server-filled defaults (auto-IDs, timestamps, computed columns, derived join columns) diverge from any client-side guess.
result - Inventing an action because the user asked for behavior the instance doesn't expose. The schema is the catalog of what exists; surface the gap, don't fake the call.
- Reaching for directly. The sandbox blocks raw network calls to the Metabase origin (raw
fetch("/api/action/...")/XHR work only for externalfetchdeclared inallowed_hosts); the only path to actions isdata_app.yaml.useAction
- 从而不是
@metabase/embedding-sdk-react导入@metabase/embedding-sdk-react/data-app。主入口的hook是通用SDK版本,接受任何类型的入参,因此永远不会执行动作定义校验。useAction - 将动作声明在根目录之外的任何地方:在hook处内联、在组件内部用
actions/包裹、或者放在defineAction(...)下。只有src/actions/目录下的内容会被同步。actions/ - 将而不是
schema.models.<model>.actions.<action>.id的导出结果传递给defineAction(...)。这样useAction会接受任何execute类型的参数,像Record<string, unknown>这样的拼写错误会被放过,并且在生产环境中会返回403错误,因为用户可运行的不是原始编写的动作。{ wrongKey: 1 } - 动作成功后忘记刷新。UI会继续显示过时的数据,没有任何错误或警告。
- 渲染/
"Failed"/"Something went wrong"来代替真实的后端错误信息。请始终提取String(error)/error.data.message(用户需要的诊断信息就在里面)并原样渲染——参见显示错误信息。error.data.errors - 调用刷新回调时没有await。模态框关闭或表单清空时,新数据还没到达,导致用户会短暂看到过时的视图。
- 在触发动作的同一个组件内加载数据。数据hook位于触发组件的下方,因此它的刷新回调无法被绑定。请将数据hook提升到父组件,并将其刷新回调传递给触发组件。
- 当参数的是
jsType或"number"时,传递原始的"boolean"字符串作为参数值。请在调用处进行类型转换。<input> - 在上一个请求还在进行时允许再次触发。请用hook返回的来控制
isExecuting状态。disabled - 为了跳过刷新,直接将动作的添加到本地列表的开头。服务器填充的默认值(自动ID、时间戳、计算列、派生的连接列)会与客户端的任何猜测都不一致。
result - 因为用户要求某个实例未提供的功能,就虚构一个动作。schema是现有功能的目录;请暴露这个差距,不要伪造调用。
- 直接使用调用接口。沙箱会阻止发往Metabase源的原始网络请求(原始的
fetch("/api/action/...")/XHR仅对fetch中声明的外部data_app.yaml有效);调用动作的唯一途径是allowed_hosts。useAction