Actions
Verbs for operational views — row, bulk, and header actions with confirm, input forms, and Cloud-safe mutations.
Views answer "what do I see." Actions answer "what can I do without opening the form." A defineAction is a single verb — Check In, Mark Paid, Assign Table, Send Reminders — that can surface as a row button, a card button, a bulk-bar button, or a view-header button.
By the end you'll know the 8 keys on ActionConfig, how mutation vs handler decides Cloud-safe vs self-hosted, and how to wire type, confirm, and fields so the button lands in the right place.
Visual outcome
All mutations run through the collection's beforeChange and afterChange hook pipeline, ensuring access checks and validation are never bypassed.
Action examples
1. Cloud-safe row action (declarative mutation)
Define a row action using a JSON mutation object:
import { defineAction } from "@dyrected/core";
export const checkInAction = defineAction({
name: "checkIn",
label: "Check In",
icon: "UserCheck",
type: "row",
confirm: "Confirm guest check-in at the door?",
mutation: { checkedIn: true, checkedInAt: "now()" },
});The "now()" expression is evaluated on the server to an ISO timestamp. Declarative mutations are serializable, making them fully compatible with Dyrected Cloud and syncable across environments.
2. Row action with an input dialog
If an action requires user input (like selecting a seat, entering notes, or recording a payment), declare fields to automatically generate an input modal.
When triggered on a row, the dialog automatically prefills with the document's current values, allowing editors to inspect and tweak existing data rather than typing from scratch:
import { defineAction, defineNumberField, defineTextField } from "@dyrected/core";
export const assignTableAction = defineAction({
name: "assignTable",
label: "Assign Table",
submitLabel: "Save Seating", // Customizes modal button (defaults to "Run")
icon: "Table2",
type: "row",
fields: [
defineNumberField({ name: "tableNumber", label: "Table Number", required: true }),
defineTextField({ name: "seatingNotes", label: "Seating Notes" }),
],
mutation: { tableNumber: "input.tableNumber", seatingNotes: "input.seatingNotes" },
});Expressions like "input.tableNumber" extract values submitted through the dialog.
3. Dynamic defaults and live modal reactivity
Action modal fields support the exact same reactivity engine and hooks as document edit forms:
- Dynamic
defaultValue(Initial Prefill): Compute initial field values using targeted record data (doc), bulk records (docs), or the authenticated editor (user). - Live
admin.hooks.onChange: Automatically recalculate calculated totals, summary strings, or reference codes as the editor types inside the modal. - Dynamic
admin.hooks.options: Cascading dropdown choices that update live as sibling inputs change. - Conditional Visibility (
admin.condition): Dynamically show or hide modal inputs based on sibling choices or targeted row properties.
Here is a complete example of a payment recording action modal with dynamic prefilling, live total calculation, and conditional cheque inputs:
import {
defineAction,
defineNumberField,
defineSelectField,
defineTextField,
expr,
} from "@dyrected/core";
export const recordPaymentAction = defineAction({
name: "recordPayment",
label: "Record Payment",
submitLabel: "Process Payment",
icon: "CreditCard",
type: "row",
fields: [
defineSelectField({
name: "paymentMethod",
label: "Payment Method",
required: true,
defaultValue: "transfer",
options: [
{ label: "Bank Transfer", value: "transfer" },
{ label: "Cash", value: "cash" },
{ label: "Cheque", value: "cheque" },
],
}),
defineTextField({
name: "chequeNumber",
label: "Cheque Number",
admin: {
// Shows only when 'cheque' is selected in the modal
condition: expr.sibling("paymentMethod").equals("cheque"),
},
}),
defineNumberField({
name: "amount",
label: "Amount (₦)",
required: true,
// 1. Initial prefill: Remaining balance from the targeted record
defaultValue: ({ doc }) => (doc?.totalDue || 0) - (doc?.amountPaid || 0),
}),
defineNumberField({
name: "balanceAfter",
label: "Remaining Balance After Payment (₦)",
admin: {
readOnly: true,
// 2. Live reactivity: Updates continuously as the editor edits 'amount'
hooks: {
onChange: ({ siblingData, doc }) => {
const remaining = (doc?.totalDue || 0) - (doc?.amountPaid || 0);
return Math.max(0, remaining - (Number(siblingData.amount) || 0));
},
},
},
}),
],
mutation: {
amountPaid: "doc.amountPaid + input.amount",
paymentMethod: "input.paymentMethod",
chequeNumber: "input.chequeNumber",
lastPaymentAt: "now()",
},
});4. Bulk and header actions
Define actions that operate across multiple selected documents or view-wide:
import { defineAction } from "@dyrected/core";
export const markSelectedPaidAction = defineAction({
name: "markSelectedPaid",
label: "Mark Paid",
submitLabel: "Confirm Payment",
type: "bulk",
mutation: { asoebiStatus: "paid", asoebiPaidAt: "now()" },
});
export const sendReminderAction = defineAction({
name: "sendReminder",
label: "Send Reminders",
submitLabel: "Send Message",
icon: "BellRing",
type: "header",
confirm: "Send a payment reminder to all guests with pending requests?",
mutation: { remindedAt: "now()" },
});Collection actions without a view
Actions don't have to belong to a view. Add an actions array directly on the collection for verbs that work anywhere one document is in scope — most notably the read-only Detail View, where they render in the header toolbar (type: "header") or inline via displayAction(name) in the detail schema:
import { defineAction, defineCollection, displayAction, displaySection } from "@dyrected/core";
const markShippedAction = defineAction({
name: "markShipped",
label: "Mark Shipped",
type: "header",
mutation: { status: "shipped" },
});
export const Orders = defineCollection({
slug: "orders",
fields: [/* ... */],
actions: [markShippedAction],
detail: [
displaySection("Fulfillment", [displayAction("markShipped", { span: 4 })]),
],
});Collection actions run through a view-less route (POST /api/collections/:slug/actions/:action), and the SDK call drops the view segment:
await client.collection("orders").runCollectionAction("markShipped", { id: order.id });The same action definition can also be referenced from a defineView's own actions — the two are not mutually exclusive.
Configuration options
| Option | Type | Description |
|---|---|---|
name | string required | Unique identifier used for ordering, logging, and SDK triggers. |
label | string required | Button text displayed to editors. |
submitLabel | string | Custom label for the modal confirmation / submission button (defaults to "Run" for form dialogs, "Confirm" for confirmation modals). |
icon | string | Lucide icon name (e.g. "UserCheck", "Table2"). |
type | "row" | "bulk" | "header" | Where the button appears. Defaults to "row". |
confirm | string | Prompt text that triggers a confirmation modal before execution. |
fields | Field[] | Input fields rendered in a modal before the action runs. For row actions, fields automatically prefill from the targeted document. |
mutation | Record<string, unknown> | Declarative mutation object with expressions ("now()", "input.fieldName", "doc.fieldName"). |
access | AccessConfig | Permissions controlling who can see or run the action. |
Button placement and ordering
- Row actions (
type: "row"): Surface on table rows, kanban cards, calendar detail sheets, and gallery card tiles. - Bulk actions (
type: "bulk"): Appear automatically in the floating bulk action bar when one or more items are selected. - Header actions (
type: "header"): Positioned at the top of the view alongside search and filter controls.
To customize the order and visibility of built-in vs custom buttons on a view:
defineView({
slug: "attending-guests",
label: "Attending Guests",
actions: [checkInAction],
features: { delete: false }, // Hides the built-in delete button
actionOrder: ["checkIn", "view", "edit"], // Places custom Check In first
});Next steps
- Run actions programmatically from code — Run an action (SDK)
- Secure actions with role permissions — Access Control
- Configure view layouts — Define a view