Skip to main content

Core Action

Self-contained, type-safe backend operations with automatic UI rendering and zero network overhead for metadata.

Key Features

  • Self-Contained: All metadata (label, icon, variant, description) lives in the action class
  • Type-Safe: Automatic TypeScript type generation with IDE autocomplete
  • Zero Network Overhead: Metadata is compiled at build time, not fetched at runtime
  • Automated Rendering: Actions automatically appear in AppBar when passed to AppPage->actions()
  • Built-in: Authorization, validation, and confirmation dialogs

Quick Start

1. Create an Action

php artisan make:core-action SendEmailAction
Define the action:
<?php

namespace App\Http\Controllers\Actions;

use Inly\Core\Actions\CoreAction;
use Inly\Core\Enums\CoreVariant;

class SendEmailAction extends CoreAction
{
    protected function defaults(): void
    {
        $this->label('Send Email');
        $this->icon('Mail');
        $this->variant(CoreVariant::BRAND);
        $this->description('Send a welcome email to the user');
    }

    public function authorize(): bool
    {
        // This method is REQUIRED - it's abstract in the base class
        return auth()->user()->can('send-emails');
    }

    protected function rules(): array
    {
        return [
            'email' => ['required', 'email'],
            'name' => ['required', 'string', 'max:255'],
        ];
    }

    protected function handle(string $email, string $name): mixed
    {
        Mail::to($email)->send(new WelcomeEmail($name));

        return redirect()->back()->withSuccess(__('Email sent to :name!', ['name' => $name]));
    }
}

2. Use in Controller

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Actions\SendEmailAction;
use Inly\Core\Pages\AppPage;

class UserController extends Controller
{
    public function show(User $user)
    {
        return AppPage::make('users/show')
            ->title($user->name)
            ->actions([
                SendEmailAction::make()->params([
                    'email' => $user->email,
                    'name' => $user->name,
                ]),
            ]);
    }
}

3. Handle on Frontend

import AppPage from '@/core/components/app-page';

export default function UserShow({ user }) {
  return <AppPage>{/* Your page content */}</AppPage>;
}
Note: Success/error toasts are automatically handled by the backend via ->withSuccess() and ->withError() redirects. You can still add custom onSuccess or onError callbacks if you need to perform additional frontend actions.

Important Details

  • Model Binding: Only model IDs are sent to the frontend. The backend automatically resolves them back to model instances in handle() using type hints.
  • Toast Messages: Handle success/error via ->withSuccess() and ->withError() in PHP, not manually in the frontend.
  • Authorization: Use getParam('key', Model::class) in authorize() to auto-resolve models for permission checks.

Action Properties

PropertyTypeMethodDescription
keystringkey()Unique identifier (auto-generated from class name)
labelstringlabel()Button label shown in UI
iconstringicon()Icon identifier (e.g., ‘Mail’, ‘Trash’)
variantCoreVariantvariant()Button style (brand, destructive, default, etc.)
descriptionstringdescription()Action description for tooltips/docs
confirmboolconfirm()Whether to show confirmation dialog
confirmMessagestringconfirmMessage()Custom confirmation message
hiddenboolhidden()Whether to hide the action from the UI
withTrashedbool|array<string, bool>withTrashed()Include soft-deleted models when resolving bindings
paramsarrayparams()Default parameters from backend

Frontend Component Props

CoreActionButton

PropTypeDescription
actionCoreActionDataThe action data object
paramsany[]Static parameters to override backend defaults
getParams() => any[]Function to compute params dynamically at execution time
onSuccess() => voidCallback fired on successful execution
onError(error: any) => voidCallback fired on error
onValidationError(errors: any) => voidCallback fired on validation errors
inlinebooleanUse inline styling for dropdowns/menus
buttonPropsButtonPropsCustom props passed to underlying button

CoreActionForm

For form-based action execution with automatic validation error handling, loading states, and confirmation dialogs, use CoreActionForm. It extends CoreForm and provides seamless integration with actions.
See: CoreActionForm Documentation for complete form integration guide.

AppPage actionCallbacks

actionCallbacks={{
  ActionKey: {
    params?: any[];
    getParams?: () => any[];
    onActionClick?: (actionData, event) => void;
    onSuccess?: () => void;
    onError?: (error) => void;
  },
}}

Parameter Handling

Parameters follow a clear priority order (highest to lowest):
  1. getParams() - Dynamic params computed at execution time (frontend)
  2. params prop - Static params passed from frontend
  3. params() - Default params set from backend controller

Backend Default Parameters

return AppPage::make('users/show')
    ->actions([
        SendEmailAction::make()->params([
            'email' => $user->email,
            'name' => $user->name,
        ]),
    ]);

Frontend Static Parameters

<CoreActionButton
  action={sendEmailAction}
  params={{ email: '[email protected]', name: 'Custom Name' }}
/>

Dynamic Parameters with getParams

<AppPage
  actionCallbacks={{
    SendEmailAction: {
      getParams: () => ({
        email: user.email,
        name: `${user.name} (email #${counter + 1})`,
      }),
      onSuccess: () => {
        setCounter(c => c + 1);
      },
    },
  }}
/>

Model Binding

Actions automatically resolve Eloquent models from IDs using type hints in the handle() method.

How It Works

Frontend receives only IDs: When you pass a model instance to an action, only the model’s ID is serialized and sent to the frontend. The backend automatically converts IDs back to model instances when the action executes. Backend automatically resolves: If your handle() method has a type-hinted Eloquent model parameter, the action will automatically resolve it using findOrFail():
protected function handle(User $user, string $reason): mixed
{
    // $user is automatically resolved from the ID
    $user->update([
        'banned_at' => now(),
        'ban_reason' => $reason,
    ]);

    return "User banned successfully";
}
Pass the ID from the controller:
BanUserAction::make()->params([
    'user' => $user->id,  // Only the ID is sent to frontend
    'reason' => 'Inappropriate behavior',
])

Including Soft-Deleted Models

By default, actions will not resolve soft-deleted models. To include trashed models when resolving model bindings, use the withTrashed() method. This follows Laravel’s route model binding pattern where you explicitly opt-in to include trashed models. Global (all models):
protected function defaults(): void
{
    $this->label('Restore User');
    $this->withTrashed(true);  // Include trashed for all model parameters
}
Per-parameter (specific models only):
protected function defaults(): void
{
    $this->label('Transfer Account');
    // Only include trashed for the 'user' parameter, not 'account'
    $this->withTrashed(['user' => true, 'account' => false]);
}
In action:
class ImpersonateUserAction extends CoreAction
{
    protected function defaults(): void
    {
        $this->label(__('Log in as...'));
        $this->withTrashed(['user' => true]);  // Allow impersonating soft-deleted users
    }

    protected function handle(User $user): mixed
    {
        // $user can be a soft-deleted user if withTrashed(['user' => true]) is set
        return redirect()->route('impersonate', $user->id);
    }
}

Using getParam() in Authorization

The getParam() protected method is useful in the authorize() method to automatically resolve models:
public function authorize(): bool
{
    // Automatically resolves the User model from the ID
    $user = $this->getParam('user', User::class);

    return auth()->user()->can('ban', $user);
}
If you pass a class name as the second parameter, getParam() will automatically resolve the Eloquent model instance from the ID using findOrFail().

Named Parameters

Use associative arrays for clearer parameter handling:
protected function rules(): array
{
    return [
        'account' => ['required', 'exists:accounts,id'],
        'end_date' => ['required', 'date', 'after_or_equal:today'],
        'reason' => ['nullable', 'string', 'max:500'],
    ];
}

protected function handle(Account $account, string $endDate, ?string $reason = null): mixed
{
    $account->cancel($endDate, $reason);
    return 'Account cancelled successfully';
}
Frontend can pass named parameters:
execute({
  params: {
    account: 123,
    end_date: '2025-12-31',
    reason: 'User requested cancellation',
  },
});

Action Dropdowns

Group multiple actions in a dropdown:
use Inly\Core\Actions\CoreActionDropdown;

return AppPage::make('users/show')
    ->actions([
        EditUserAction::make()->params(['id' => $user->id]),

        CoreActionDropdown::make([
            SendEmailAction::make()->params([
                'email' => $user->email,
                'name' => $user->name,
            ]),
            ResetPasswordAction::make()->params(['id' => $user->id]),
            BanUserAction::make()->params(['user' => $user->id]),
            DeleteUserAction::make()->params(['id' => $user->id]),
        ], label: 'More Actions'),
    ]);

Primary Actions

Mark actions or dropdowns as primary to show them in the page header. Non-primary actions will only appear in the AppBar (sticky header that appears on scroll):
return AppPage::make('accounts/show')
    ->title($account->name)
    ->actions([
        // This action will appear as a button in the page header
        ApproveAccountAction::make()
            ->params(['id' => $account->id])
            ->primary(),

        // This entire dropdown will appear in the page header
        CoreActionDropdown::make([
            EditAccountAction::make()->params(['id' => $account->id]),
            DeleteAccountAction::make()->params(['id' => $account->id]),
        ], label: 'More Actions')
            ->primary(),

        // Primary actions from this dropdown will be EXTRACTED and shown as individual buttons
        CoreActionDropdown::make([
            ApproveAccountAction::make()->params(['id' => $account->id])->primary(),  // Shown as button
            RejectAccountAction::make()->params(['id' => $account->id])->primary(),   // Shown as button
            ArchiveAccountAction::make()->params(['id' => $account->id]),            // Only in AppBar
        ], label: 'Account Actions'),

        // This action will only appear in the AppBar (on scroll)
        ExportAccountAction::make()->params(['id' => $account->id]),
    ]);

Primary Actions Behavior

For individual actions:
  • Actions marked with ->primary() appear as buttons in the page header
For dropdowns:
  1. If the dropdown itself is marked as ->primary(): The entire dropdown appears in the header
  2. If individual actions inside are marked as ->primary(): Those actions are extracted and shown as individual buttons (not in a dropdown)
Visual Indicator:
  • When primary actions are extracted from a dropdown that contains additional non-primary actions, a vertical dashed border separator appears between the title and action buttons to indicate more actions are available in the AppBar
This is useful when you have many actions but only want to highlight the most important ones in the page header. All actions will still appear in the AppBar when the user scrolls down.

Passing Actions Via ->with()

Sometimes you want to pass an action to the frontend without registering it in the AppBar. Use ->with() to pass actions as data:
return AppPage::make('users/show')
    ->title($user->name)
    ->actions([
        EditUserAction::make()->params(['id' => $user->id]),
    ])
    ->with([
        'user' => $user,
        'sendEmailAction' => SendEmailAction::make()->params([
            'email' => $user->email,
            'name' => $user->name,
        ]),
    ]);
On the frontend, you can access the action and use it anywhere in your component:
import CoreActionButton from '@/core/components/core-action-button';

export default function UserShow({ user, sendEmailAction }) {
  return (
    <div>
      <h1>{user.name}</h1>

      {/* Use the action wherever you want */}
      <CoreActionButton action={sendEmailAction} />
    </div>
  );
}

Standalone Usage

Using CoreActionButton

import CoreActionButton from '@/core/components/core-action-button';

export default function UserShow({ sendEmailAction }) {
  return (
    <div>
      <h1>{user.name}</h1>

      <CoreActionButton action={sendEmailAction} />
    </div>
  );
}

Using CoreActionForm

For form-based action execution, use CoreActionForm. It provides automatic validation error handling, loading states, and confirmation dialogs.
See: CoreActionForm Documentation for complete usage examples and API reference.

useCoreAction Hook

For React components that need loading state management:
import { useCoreAction } from '@/core/hooks/use-core-action';

export default function UserShow({ sendEmailAction }) {
  const { execute, loading } = useCoreAction(sendEmailAction, {
    onSuccess: () => {
      // Custom success handler if needed
      console.log('Email sent!');
    },
    onError: error => {
      // Custom error handler if needed
      console.error('Error:', error);
    },
  });

  return (
    <button onClick={() => execute()} disabled={loading}>
      Send Email
    </button>
  );
}

executeCoreAction Function

For imperative execution in callbacks, event handlers, or anywhere hooks can’t be used:
import { executeCoreAction } from '@/core/hooks/use-core-action';

// Simple execution with default params
executeCoreAction(action);

// With custom params
executeCoreAction(action, {
  params: { email: '[email protected]' },
});

// With callbacks
executeCoreAction(action, {
  onSuccess: () => console.log('Done!'),
  onError: error => console.error(error),
  onValidationError: errors => console.warn('Validation failed:', errors),
});
This is useful in:
  • useMemo callbacks where hooks can’t be called
  • Event handlers that don’t need loading state
  • Quick actions in global search
  • Any imperative context outside React’s render cycle

Return Types

Actions can return various response types:
// String message - redirects back with success message
return 'Action completed successfully!';

// Redirect with success toast
return redirect()->back()->withSuccess(__('User created!'));

// Redirect with error toast
return redirect()->back()->withError(__('Something went wrong'));

// JSON data
return response()->json([
    'success' => true,
    'data' => ['id' => 123],
]);

// Download
return response()->download($path);

Complete Example

Backend Action:
<?php

namespace App\Http\Controllers\Actions;

use App\Models\User;
use Inly\Core\Actions\CoreAction;
use Inly\Core\Enums\CoreVariant;

class BanUserAction extends CoreAction
{
    protected function defaults(): void
    {
        $this->label('Ban User');
        $this->icon('Ban');
        $this->variant(CoreVariant::DESTRUCTIVE);
        $this->confirm(true);
        $this->confirmMessage('Are you sure you want to ban this user?');
    }

    public function authorize(): bool
    {
        // This method is REQUIRED - it's abstract in the base class
        return auth()->user()->can('ban-users');
    }

    protected function rules(): array
    {
        return [
            'user' => ['required', 'exists:users,id'],
            'reason' => ['required', 'string', 'max:500'],
        ];
    }

    protected function handle(User $user, string $reason): mixed
    {
        $user->update([
            'banned_at' => now(),
            'ban_reason' => $reason,
        ]);

        return redirect()->back()->withSuccess(__('User :name has been banned.', ['name' => $user->name]));
    }
}
Controller:
<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Actions\BanUserAction;
use App\Models\User;
use Inly\Core\Pages\AppPage;

class UserController extends Controller
{
    public function show(User $user)
    {
        return AppPage::make('users/show')
            ->title($user->name)
            ->actions([
                BanUserAction::make()->params([
                    'user' => $user->id,
                    'reason' => 'Inappropriate behavior',
                ]),
            ])
            ->with(['user' => $user]);
    }
}
Frontend:
import AppPage from '@/core/components/app-page';

export default function UserShow({ user }) {
  return (
    <AppPage>
      <div className="space-y-6">
        <div>
          <h3>User Details</h3>
          <dl>
            <dt>Name</dt>
            <dd>{user.name}</dd>
            <dt>Email</dt>
            <dd>{user.email}</dd>
          </dl>
        </div>
      </div>
    </AppPage>
  );
}

TypeScript Types

Run to generate TypeScript types:
php artisan types:transform
This generates type definitions in resources/js/types/generated.d.ts for IDE autocomplete.

Hiding Actions

Actions can be hidden from the UI while still being executable programmatically. Use hidden() to hide an action:
protected function defaults(): void
{
    $this->label('Internal Action');
    $this->hidden(true);  // Hide from UI, but still executable
}
Hidden actions:
  • Are not rendered in the AppBar
  • Are not included in global search quick actions
  • Are not shown in dropdown menus
  • Can still be executed programmatically if accessed directly
This is useful for internal actions that should only be triggered programmatically, not through the UI.

WithTableAction Trait

For actions that operate on table rows (single or bulk), use the WithTableAction trait. It provides automatic authorization, validation, and query building - similar to how WithTableQuery works for Form Requests.

When to Use WithTableAction

Use WithTableAction when:
  • Your action operates on one or more table rows
  • You need to support both row actions (single item) and bulk actions (multiple items)
  • You want automatic id/ids validation
  • You want per-model authorization checks

Basic Usage

<?php

namespace App\Actions;

use App\Models\User;
use App\Tables\UserTable;
use Inly\Core\Actions\CoreAction;
use Inly\Core\Actions\Traits\WithTableAction;
use Inly\Core\Enums\CoreVariant;

class DeactivateUsersAction extends CoreAction
{
    use WithTableAction;

    // Required: Define the table to enable '*' (select all) and model inference
    protected function getTable(): string
    {
        return UserTable::class;
    }

    protected function defaults(): void
    {
        $this->label(__('Deactivate'));
        $this->icon('UserX');
        $this->variant(CoreVariant::DESTRUCTIVE);
        $this->confirm(true);
        $this->confirmMessage(__('Are you sure you want to deactivate the selected users?'));
        $this->asBulkAction(true);
    }

    // Optional: Custom per-model authorization
    protected function authorizeModel(User $model): bool
    {
        return user()?->can('update', $model) ?? false;
    }

    // Optional: Add custom validation rules (id/ids included by default)
    protected function rules(): array
    {
        return [
            ...$this->idRules(),
            'reason' => ['nullable', 'string', 'max:500'],
        ];
    }

    // Use processModels() for clean, automatic count tracking
    protected function handle(?string $reason = null): mixed
    {
        $this->processModels(function (User $user) use ($reason) {
            $user->update([
                'active' => false,
                'deactivation_reason' => $reason,
            ]);
        });

        return $this->backWithSuccess();
    }
}

Trait Methods

MethodTypeDescription
getTable()RequiredReturns table class for model inference and * (select all) support
authorizeModel(Model $model)OptionalPer-model authorization check (default: true)
authorizeBulk()OptionalBulk authorization check for performance (default: null)
authorize()ProvidedIterates all targets, calls authorizeModel() for each
rules()ProvidedReturns idRules() by default, override to add custom rules
idRules()ProvidedReturns validation rules for id/ids parameters
getTableQuery()ProvidedQuery builder scoped to target IDs
getModel()ProvidedGet single model (for single-item actions)
getModels()ProvidedGet all target models as collection
getCount()ProvidedGet count of affected records
processModels()ProvidedExecute callback on single/bulk models, auto-tracks count
getBulkMessage()ProvidedGenerate success message (singular/plural, count-aware)
backWithSuccess()ProvidedRedirect with success message (uses getBulkMessage())
isBulkOperation()ProvidedCheck if action has ids array
hasSingleId()ProvidedCheck if action has single id

Handling Single vs Bulk Operations

The trait automatically handles both single (id) and bulk (ids) operations. Use processModels() for clean, automatic handling:
// Frontend can send either:
// Single: { id: 123 }
// Bulk:   { ids: [1, 2, 3] }
// All:    { ids: ['*'] }  // Select all with table filters

protected function handle(): mixed
{
    // processModels() works for all cases - no conditionals needed
    $this->processModels(function ($model) {
        // Process each model
        $model->update(['processed' => true]);
    });

    return $this->backWithSuccess();
}

Using processModels() Helper

The processModels() method is the recommended way to handle both single and bulk operations. It simplifies your code by automatically:
  • Detecting whether it’s a single or bulk operation
  • Executing your callback on each model
  • Tracking the count for success messages
  • Providing a consistent pattern across all table actions
protected function handle(): mixed
{
    $this->processModels(function ($model) {
        $model->update(['status' => 'processed']);
    });

    return $this->backWithSuccess();
}
Benefits over manual iteration:
  • Cleaner, more readable code
  • Automatic count tracking for getBulkMessage()
  • Consistent pattern across all table actions
  • No need for conditional logic or manual counting
  • Easier to maintain and test
Example: Restore Soft-Deleted Action
<?php

namespace Inly\Core\Actions;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use InertiaUI\Table\Table;
use Inly\Core\Actions\Traits\WithTableAction;
use Inly\Core\Enums\CoreVariant;

abstract class RestoreSoftDeletedAction extends CoreAction
{
    use WithTableAction {
        getFilteredQuery as protected parentGetFilteredQuery;
        getBulkMessage as protected parentGetBulkMessage;
    }

    protected function defaults(): void
    {
        $this->label(__('Restore'));
        $this->icon('RotateCcw');
        $this->variant(CoreVariant::SUCCESS);
        $this->confirm();
        $this->asBulkAction();
        $this->withTrashed();  // Include trashed models
    }

    protected function authorizeModel(Model $model): bool
    {
        return $model->trashed();
    }

    protected function handle(): mixed
    {
        // Simple, clean callback - processModels() handles everything
        $this->processModels(function ($model) {
            $model->restore();
        });

        return $this->backWithSuccess();
    }

    // Ensure trashed models are included in queries
    protected function getFilteredQuery(Table $table, ?array $queryParams = null): Builder
    {
        return $this->parentGetFilteredQuery($table, $queryParams)->withTrashed();
    }

    // Customize success message
    public function getBulkMessage(?string $rowSingular = null, ?string $rowPlural = null, ?string $actionLabel = null): string
    {
        return $this->parentGetBulkMessage(
            $this->getRowSingular() ?? $rowSingular,
            $this->getRowPlural() ?? $rowPlural,
            $actionLabel ?? __('was restored successfully')
        );
    }

    // Child classes override to customize model names
    protected function getRowSingular(): ?string
    {
        return null;
    }

    protected function getRowPlural(): ?string
    {
        return null;
    }
}
Child implementation:
class RestoreUserAction extends RestoreSoftDeletedAction
{
    protected function getTable(): Table|string
    {
        return UserTable::class;
    }

    protected function authorizeModel(Model $model): bool
    {
        return user()?->can('restore', $model)
            && parent::authorizeModel($model);
    }

    protected function getRowSingular(): string
    {
        return __('The user');
    }

    protected function getRowPlural(): string
    {
        return __('Users');
    }
}
This produces messages like:
  • Single: “The user was restored successfully”
  • Bulk: “5 users were restored successfully”

Customizing Success Messages

Override getBulkMessage() to customize the success message format:
public function getBulkMessage(?string $rowSingular = null, ?string $rowPlural = null, ?string $actionLabel = null): string
{
    return parent::getBulkMessage(
        $rowSingular ?? __('The transaction'),
        $rowPlural ?? __('Transactions'),
        $actionLabel ?? __('were processed successfully')
    );
}
The method automatically handles singular vs plural based on the count:
  • Count = 1: Uses $rowSingular (e.g., “The user was restored”)
  • Count > 1: Uses $rowPlural (e.g., “5 users were restored”)

Manual Query Iteration (Alternative)

If you need more control than processModels() provides, you can manually iterate with getTableQuery():
protected function handle(): mixed
{
    $count = 0;

    foreach ($this->getTableQuery()->cursor() as $model) {
        $model->update(['processed' => true]);
        $count++;
    }

    $this->setMessageCount($count);

    return $this->backWithSuccess();
}
Note: Manual iteration requires calling setMessageCount() before backWithSuccess(). Use processModels() instead for automatic count tracking.

Select All (*) Support

When users select all items in a table, the frontend sends ids: ['*']. The trait automatically applies table filters and search from the previous URL:
// Required for '*' support
protected function getTable(): string
{
    return UserTable::class;
}

// Now when frontend sends { ids: ['*'] }:
// - Table filters are applied (e.g., status=active)
// - Search terms are applied
// - Only matching records are affected

Comparison with WithTableQuery

FeatureWithTableQuery (Form Request)WithTableAction (CoreAction)
Used inForm RequestsCoreActions
Input source$this->input()$this->params
AuthorizationManual in controllerAutomatic via authorizeModel()
ValidationManual rules()Auto id/ids via idRules()
Query buildinggetTableQuery()getTableQuery()
Both traits share the same query-building logic for consistency.

Inline Edit Column Integration

Use CoreActions with InlineEditColumn for inline table editing:
InlineEditColumn::make('title', __('Title'))
    ->coreAction(UpdateAddonAction::class, fn($addon) => ['id' => $addon->id])
See also: Inertia Table - InlineEditColumn

Known Issues & Limitations

Dynamic Action Properties Not Persisted

Properties set dynamically in controllers (e.g., ->withTrashed(), ->label()) are not available during action execution. Only properties defined in defaults() and params are persisted. Workaround: Define all properties in the defaults() method or use conditional logic there for different contexts.

Best Practices

  1. Use Descriptive Names: SendWelcomeEmailToNewUserAction not EmailAction
  2. Set Defaults in defaults(): All metadata should be configured here
  3. Implement authorize(): This method is REQUIRED (abstract in base class) - always return a boolean
  4. Validate Parameters: Always define validation rules for parameters
  5. Use Model Binding: Type-hint Eloquent models in handle() - only IDs are sent to frontend, models auto-resolve
  6. Use getParam() for Authorization: In authorize(), use $this->getParam('key', Model::class) to auto-resolve models
  7. Return Meaningful Messages: Use ->withSuccess() and ->withError() for toast messages
  8. Add Confirmation: Use for destructive actions
  9. Set Backend Params Explicitly: Use ->params(['key' => 'value']) with associative arrays to explicitly set parameter keys
  10. Use getParams for UI State: For dynamic params based on frontend state
  11. Keep Actions Focused: One action should do one thing
  12. Pass Actions via ->with(): When you need to use actions without registering them in the AppBar
  13. Use withTrashed() Explicitly: Only include soft-deleted models when necessary, matching route behavior
  14. Hide Internal Actions: Use hidden() for actions that should only be executed programmatically
  15. Use WithTableAction for Table Operations: Simplifies single/bulk operations with automatic authorization and validation
  16. Prefer processModels() Over Manual Iteration: Use processModels() for cleaner code with automatic count tracking