Dyrected
Model ContentOperational Views

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

OptionTypeDescription
namestring requiredUnique identifier used for ordering, logging, and SDK triggers.
labelstring requiredButton text displayed to editors.
submitLabelstringCustom label for the modal confirmation / submission button (defaults to "Run" for form dialogs, "Confirm" for confirmation modals).
iconstringLucide icon name (e.g. "UserCheck", "Table2").
type"row" | "bulk" | "header"Where the button appears. Defaults to "row".
confirmstringPrompt text that triggers a confirmation modal before execution.
fieldsField[]Input fields rendered in a modal before the action runs. For row actions, fields automatically prefill from the targeted document.
mutationRecord<string, unknown>Declarative mutation object with expressions ("now()", "input.fieldName", "doc.fieldName").
accessAccessConfigPermissions 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

On this page

Dyrected| Cloud

Get your backend ready in minutes

Use a managed database, storage, APIs, and admin dashboard without setting up the infrastructure yourself.

Set Up My Backend