# Create Resource Action

> Use when creating Backpex resource actions for global operations like bulk exports, invitations, imports, or any action that applies to the resource as a whole rather than individual items.

- Skill: `naymspace/create-resource-action` (Agent Skill)
- Install (CLI): `npx skillmds@latest add naymspace/create-resource-action`
- Raw SKILL.md: https://api.skillmd.com/api/skills/naymspace/create-resource-action/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: naymspace (https://skillmd.com/u/naymspace)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/naymspace/create-resource-action

---


# Creating Backpex Resource Actions

You are an expert at creating resource actions for Backpex. Resource actions operate on the resource as a whole (not individual items) and appear as buttons in the index toolbar. They open a slide-over with a form.

## Required Callbacks

| Callback | Signature | Description |
|----------|-----------|-------------|
| `title/0` | `-> string` | Slide-over title |
| `label/0` | `-> string` | Button text in index toolbar |
| `fields/0` | `-> keyword list` | Form field definitions |
| `changeset/3` | `(change, attrs, metadata) -> changeset` | Validate form data. `metadata` has `:assigns` and `:target` keys |
| `handle/2` | `(socket, data) -> {:ok, socket} \| {:error, changeset}` | Execute the action with validated data |

## Optional Callbacks

| Callback | Default | Description |
|----------|---------|-------------|
| `base_schema/1` | schemaless changeset | Override to use a real Ecto schema |

## Example: Simple Resource Action

```elixir
defmodule MyAppWeb.ResourceActions.InviteUser do
  use Backpex.ResourceAction

  import Ecto.Changeset

  @impl Backpex.ResourceAction
  def title, do: "Invite User"

  @impl Backpex.ResourceAction
  def label, do: "Invite"

  @impl Backpex.ResourceAction
  def fields do
    [
      email: %{
        module: Backpex.Fields.Text,
        label: "Email",
        type: :string
      },
      role: %{
        module: Backpex.Fields.Select,
        label: "Role",
        options: [Admin: "admin", User: "user"],
        prompt: "Select role...",
        type: :string
      }
    ]
  end

  @impl Backpex.ResourceAction
  def changeset(change, attrs, _metadata) do
    change
    |> cast(attrs, [:email, :role])
    |> validate_required([:email, :role])
    |> validate_format(:email, ~r/@/)
  end

  @impl Backpex.ResourceAction
  def handle(socket, data) do
    case MyApp.Accounts.send_invitation(data.email, data.role) do
      :ok ->
        {:ok, Phoenix.LiveView.put_flash(socket, :info, "Invitation sent to #{data.email}.")}

      {:error, reason} ->
        {:ok, Phoenix.LiveView.put_flash(socket, :error, "Failed: #{reason}")}
    end
  end
end
```

## Example: Export Action

```elixir
defmodule MyAppWeb.ResourceActions.ExportPosts do
  use Backpex.ResourceAction

  import Ecto.Changeset

  @impl Backpex.ResourceAction
  def title, do: "Export Posts"

  @impl Backpex.ResourceAction
  def label, do: "Export"

  @impl Backpex.ResourceAction
  def fields do
    [
      format: %{
        module: Backpex.Fields.Select,
        label: "Format",
        options: [CSV: "csv", JSON: "json"],
        prompt: "Select format...",
        type: :string
      }
    ]
  end

  @impl Backpex.ResourceAction
  def changeset(change, attrs, _metadata) do
    change
    |> cast(attrs, [:format])
    |> validate_required([:format])
    |> validate_inclusion(:format, ["csv", "json"])
  end

  @impl Backpex.ResourceAction
  def handle(socket, data) do
    # Trigger export...
    {:ok, Phoenix.LiveView.put_flash(socket, :info, "Export started in #{data.format} format.")}
  end
end
```

## Wiring Into a LiveResource

```elixir
@impl Backpex.LiveResource
def resource_actions do
  [
    invite: %{module: MyAppWeb.ResourceActions.InviteUser},
    export: %{module: MyAppWeb.ResourceActions.ExportPosts}
  ]
end
```

The keyword key (e.g. `:invite`) is used as the action identifier for routing and authorization via `can?/3`.

## Key Differences From Item Actions

| Aspect | Resource Action | Item Action |
|--------|----------------|-------------|
| Scope | Whole resource | Selected items |
| UI | Slide-over form | Modal dialog |
| Callbacks | `title/0`, `label/0`, `handle/2` | `icon/2`, `label/2`, `handle/3` |
| Form | Always has fields | Optional |
| Location | Index toolbar only | Row, index toolbar, show page |

## Conventions

- **File location**: `lib/my_app_web/resource_actions/<snake_case_name>.ex`
- **Module naming**: `MyAppWeb.ResourceActions.<ActionName>`
- **Always include `type:` key** in each field map (e.g. `type: :string`). This is required for the schemaless changeset to work.
- **Authorization** is handled via `can?(assigns, :action_key, nil)` in the LiveResource (item is always `nil`)
- **Return `{:error, changeset}`** from `handle/2` to keep the form open and show validation errors

