Browse documentation

Actions

Row, bulk, dashboard and drawer actions — every option and what they return.

An action is a server-side function the operator triggers from the UI. FlowPanel gives you four places to put one, all sharing the same result type and the same guard pipeline: the admin-wide role gate, the resource gate, then the action's own requireRole — before run is ever called.

Actions never become Server Actions. They are POST routes under /api/flowpanel/…, mounted by handlers(config).

RowAction

Type-preserving helper for a row action with a dedicated form payload.

export function rowAction<Row, Input extends ActionInput = ActionInput, Output = never>(definition: RowAction<Row, Input, Output>): RowAction<Row, Input, Output>;

Offered on a single row, inline or in the row menu.

import { resource, rowAction } from "@flowpanel/kit";
import { orders } from "@/db/schema";

type RefundInput = { reason: string };

const refundOrder = rowAction<typeof orders.$inferSelect, RefundInput>({
  key: "refund",
  label: "Refund",
  icon: "circle-dollar-sign",
  variant: "destructive",
  confirm: { title: "Refund this order?", confirmLabel: "Refund" },
  requireRole: "admin",
  disabled: (row) => (row.status === "refunded" ? "Already refunded" : false),
  form: [{ name: "reason", label: "Reason", type: "textarea", required: true }],
  run: async (row, input, ctx) => {
    await refund(ctx.db, row.id, input.reason);
    return { ok: true, message: `Refunded ${row.id}`, refresh: true };
  },
});

resource(orders, {
  columns: ["id", "status"],
  actions: [refundOrder],
});

The form describes an action-specific ActionInput, not columns on the row. Use rowAction<Row, Input>() to get exact types for both handler arguments and compile-time checks for every form[].name. The helper returns the same plain object you could write inline; it has no runtime cost.

import {  } from "@flowpanel/kit";

type  = { : string; : "active" | "suspended" };
type  = { : string; : boolean };

export const  = <, >({
  : "suspend",
  : "Suspend",
  : "ban",
  : [
    { : "reason", : "textarea", : true },
    { : "notify", : "checkbox" },
  ],
  : async (, ) => ({
    : true,
    : `${.}: ${.}`,
  }),
});

Prop

Type

hidden runs server-side for every row on the page, and the row's action list is filtered before it reaches the client — a hidden action is not merely invisible, its route returns 404 for that row.

BulkAction

Type-preserving helper for a bulk action with a dedicated form payload.

export function bulkAction<Row, Input extends ActionInput = ActionInput, Output = never>(definition: BulkAction<Row, Input, Output>): BulkAction<Row, Input, Output>;

Applied to the operator's selection. run receives the ids, capped at 1000 per request — the cap is a rejection, not a truncation: over 1000 ids the route answers 422 too many ids (max 1000) and run never executes, so nothing is half-applied. Page your own batches if you need more.

Prop

Type

Use bulkAction<Row, Input>(definition) when the bulk form has a typed payload. Its handler receives string[] ids followed by that input.

When a resource has delete enabled and declares no bulkActions, defineAdmin adds a default delete action. Opt out with bulkActions: [].

DashboardAction

Type-preserving helper for a dashboard action with a dedicated form payload.

export function dashboardAction<Input extends ActionInput = ActionInput, Output = never>(definition: DashboardAction<Input, Output>): DashboardAction<Input, Output>;

Rendered in a dashboard's page header. It gets no row — only the form input.

Prop

Type

Use dashboardAction<Input>(definition) for the same typed form contract without a row type.

Set hideActionsBar: true on the dashboard to suppress the bar these render in.

DrawerAction

Rendered in the drawer footer for the open row. Its form uses the lighter DrawerFieldFormSpec rather than FieldDef.

Prop

Type

DrawerFieldFormSpec

Prop

Type

ActionResult

What every run returns. The success shape drives what the operator sees next.

Prop

Type

ActionInput is the default Record<string, unknown> payload used when no more specific input type is supplied.

Returning { ok: false, error } is the way to fail a run cleanly — the message reaches the operator verbatim, so keep internals out of it. Throwing works too: a FlowpanelError maps to its own status, anything else becomes a generic 500 with the detail logged rather than sent.

ActionContext

The last argument to every run.

Prop

Type

ctx.db is your own client — the adapter hands it through untouched, so you write ordinary queries. ctx.publish pushes a realtime message to every connected admin; widgets and lists that name the channel in realtime refresh.

Validation

An action with a form is validated server-side before run:

  1. required fields must be present.
  2. Each field's validate runs — a Zod schema or a function returning a message.
  3. Only then does run receive input.

A failure returns 422 with an issues array. The client renders them against the matching inputs.