Custom Actions & Row Operations in actions/*.ts #

Custom Listing Actions allow developers to attach executable server-side TypeScript functions to custom listing pages. Actions can operate on individual records (via table row dropdown menus) or execute batch operations across multiple selected records (via the floating bulk action bar).

Like fetchers and dynamic filter resolvers, all actions execute inside secure, memory-capped V8 isolate sandboxes with full access to the dolfin CRM & HTTP SDK.


Action Types: Row vs. Bulk (Rows) #

When creating an action in the Custom Functions IDE (actions folder), you select one of two execution modes:

Action ModeTrigger LocationContext InputBest Used For
Single-Row Action ("row")Table row kebab menu (...)context.row (single record)Changing status, sending an email to a lead, syncing a single record to an ERP, opening details.
Bulk Action ("rows")Floating Bulk Action Barcontext.rows (array of selected records)Batch status updates, mass tagging, exporting selected records, batch webhook dispatch.

Primary Key Configuration (isPrimaryKey) #

For the listing table to track selected rows, render multi-select checkboxes, and correctly identify records during action execution, at least one column in your filters.ts should be designated as the primary key using isPrimaryKey: true:

typescript
// filters.ts
export const columns = defineColumns([
  { key: "id", label: "ID", type: "number", width: 80, isPrimaryKey: true },
  { key: "name", label: "Customer Name", type: "text" },
  { key: "email", label: "Email Address", type: "text" },
  { key: "status", label: "Status", type: "badge" },
]);
💡 Tip

If no column has isPrimaryKey: true, the table defaults to using the id field. Always mark isPrimaryKey: true explicitly when using custom unique identifiers (e.g. uuid, code, or leadId).


Single-Row Actions (type: "row") #

Single-row actions export an asynchronous run function that receives CustomListRowActionContext and DolfinSdk.

Context Signature #

typescript
interface CustomListRowActionContext {
  type: "row";
  /** Strongly typed single record data inferred from columns schema in filters.ts */
  row: __ListingRow;
  /** Active filter values currently selected in the UI */
  filter: __ListingFilters;
  /** Active tenant identifier */
  tenantId: number;
  /** Target module identifier (or null for standalone listing pages) */
  moduleId: number | null;
  /** Decrypted secrets scoped to this listing page */
  secrets: CustomSecrets;
}

Single-Row Action Example: Qualify Lead & Log Note #

typescript
// actions/qualify-lead.ts
export async function run(
  context: CustomListRowActionContext,
  dolfin: DolfinSdk
): Promise<CustomListActionResponse> {
  const { row, moduleId } = context;

  if (!moduleId) {
    throw new Error("Module ID is required to execute this action.");
  }

  // 1. Access module CRM API
  const crm = await dolfin.crm.module(moduleId);

  // 2. Update lead status in CRM
  const updatedLead = await crm.leads.update(Number(row.id), {
    meta: { status: "qualified" },
  });

  // 3. Append internal note
  await crm.leads.notes.add(
    Number(row.id),
    `Lead marked as Qualified by custom listing action.`
  );

  // 4. Return user feedback & trigger table refresh
  return {
    success: true,
    message: `Lead "${row.name}" successfully qualified!`,
    refresh: true, // Automatically reloads the listing table data
  };
}

Bulk Actions (type: "rows") #

Bulk actions execute when users select one or more records using the table checkboxes and click an action in the floating action bar.

Context Signature #

typescript
interface CustomListRowsActionContext {
  type: "rows";
  /** Strongly typed array of selected records inferred from columns schema in filters.ts */
  rows: __ListingRow[];
  /** Active filter values currently selected in the UI */
  filter: __ListingFilters;
  /** Active tenant identifier */
  tenantId: number;
  /** Target module identifier (or null for standalone listing pages) */
  moduleId: number | null;
  /** Decrypted secrets scoped to this listing page */
  secrets: CustomSecrets;
}

Bulk Action Example: Batch Webhook Sync #

typescript
// actions/bulk-sync.ts
export async function run(
  context: CustomListRowsActionContext,
  dolfin: DolfinSdk
): Promise<CustomListActionResponse> {
  const { rows } = context;

  if (rows.length === 0) {
    return { success: false, message: "No rows selected." };
  }

  // 1. Dispatch batch webhook to external synchronization service
  const response = await dolfin.http.post(
    "https://api.external-erp.example.com/v1/batch-sync",
    {
      items: rows.map((r) => ({
        id: r.id,
        name: r.name,
        email: r.email,
      })),
    },
    {
      headers: {
        "Authorization": `Bearer ${context.secrets.ERP_API_KEY}`,
        "Content-Type": "application/json",
      },
      timeoutMs: 8000,
    }
  );

  // 2. Return toast notification and trigger table refresh
  return {
    success: true,
    message: `Successfully synchronized ${rows.length} records to external ERP!`,
    refresh: true,
  };
}

Action Response Contract (CustomListActionResponse) #

Your action's run function should return a CustomListActionResponse object to provide user feedback:

typescript
interface CustomListActionResponse {
  /** Indicates whether the action completed successfully (defaults to true) */
  success?: boolean;
  /** Message displayed to the user in a toast notification */
  message?: string;
  /** When true, the frontend automatically reloads the table data after execution */
  refresh?: boolean;
  /** Optional URL path to navigate the user to after completion */
  redirectUrl?: string;
}

Toast Feedback & Table Reloading #

  • When success: true (or omitted), the frontend displays a Success Toast with your custom message.
  • When success: false or if an unhandled error is thrown, the frontend displays an Error Toast with the failure message.
  • Setting refresh: true causes the listing table to automatically refetch data using the active filters, reflecting any database changes instantly.

Authoring Actions in the IDE #

  1. Open your Custom Listing Page in the Custom Functions IDE.
  2. In the explorer sidebar, expand the actions folder under your listing page.
  3. Click the + (New Action) icon on the actions folder.
  4. Fill in the action details in the modal:
    • Action Name: Human-readable label (e.g. Qualify Lead or Export Selected).
    • Action Key / File: Filename identifier (e.g. qualify-lead.ts).
    • Action Mode: Choose Single Row (row) or Bulk Selected Rows (rows).
  5. The IDE generates a starter template pre-typed for your action mode.
  6. Write your custom logic and press Ctrl+S to save and compile.