Custom Actions

Add your own buttons next to the built-in ones: publish a draft, archive the selected rows, sync a resource with another system.

Custom Actions

Add your own buttons next to the built-in ones: publish a draft, archive the selected rows, sync a resource with another system.

Bulk delete, CSV/JSON export and CSV import are built in — see Features.

Where they live

An action's handler is a function, and a function in nuxt.config cannot reach the running app. So actions are defined in their own file, which is bundled with the admin: app/admin.actions.ts (the app's source directory — admin.actions.ts next to app.vue).

app/admin.actions.ts
export default defineAdminActions({
  articles: {
    publish: {
      label: 'Publish',
      icon: 'i-heroicons-rocket-launch',
      type: 'single',
      location: ['row', 'detail'],
      permission: 'update',
      confirm: article => `Publish "${article.title}"?`,
      handler: async (article, ctx) => {
        await $fetch(ctx.path(article.id), { method: 'PATCH', body: { published: true } })
        await ctx.refresh()
        ctx.toast.success(`"${article.title}" is live`)
      },
    },
  },
})

The keys are resource names, then action names. defineAdminActions is auto-imported.

To keep the file somewhere else, point the module at it:

nuxt.config.ts
autoAdmin: {
  actions: '~/admin/actions', // resolved like an import
}

The file is found when the app starts: after creating it, restart the dev server.

resources.<name>.actions in nuxt.config fails the build with a pointer to this file — earlier versions documented it there, where it was never applied.

Action types

Single — one record

publish: {
  label: 'Publish',
  type: 'single',
  location: ['row', 'detail'],
  handler: async (article, ctx) => { /* … */ },
}
  • location: 'row' — in each row's menu, after View / Edit / Delete
  • location: 'detail' — on the record: the detail page and the view modal

The handler gets the record.

Bulk — the selected records

unpublish: {
  label: 'Unpublish',
  icon: 'i-heroicons-eye-slash',
  type: 'bulk',
  location: 'toolbar',
  confirm: articles => `Unpublish ${articles.length} articles?`,
  handler: async (articles, ctx) => {
    await Promise.all(articles.map(a =>
      $fetch(ctx.path(a.id), { method: 'PATCH', body: { published: false } }),
    ))
    await ctx.refresh()
    ctx.toast.success(`${articles.length} unpublished`)
  },
}

A button on the list toolbar while rows are selected — Unpublish (2). The handler gets the selected rows of the current page. Rows become selectable for a bulk action even when the caller may not delete them (bulk delete still only takes the rows they may delete). features.bulkActions: false turns selection off, custom bulk actions included.

Page-level — the resource

syncFromCrm: {
  label: 'Sync from CRM',
  icon: 'i-heroicons-arrow-path',
  type: 'page-level',
  location: 'toolbar',
  handler: async (_, ctx) => {
    await $fetch('/api/crm/sync', { method: 'POST' })
    await ctx.refresh()
    ctx.toast.success('Synced')
  },
}

A button next to "Create New". The handler gets undefined.

Options

Option
labelButton / menu text
iconIcon name, e.g. i-heroicons-rocket-launch
type'single' · 'bulk' · 'page-level'
location'row' · 'detail' (or both, as an array) for single; bulk and page-level are always on the toolbar
permissionWho may use it — see below
confirmAsk first: a message, or a function of the record(s)
variantNuxt UI button variant: solid · outline · soft · subtle · ghost · link
colorNuxt UI color: primary · secondary · success · info · warning · error · neutral
handler(item, ctx) => void | Promise<void>

Permissions

An operation is checked against the API's own answer — the same one the built-in buttons use:

permission: 'update', // 'create' | 'read' | 'update' | 'delete'

For a row or a record, update and delete are that row's answer (its objectLevel rule included), so the action is disabled exactly where Edit is.

Or a function, for anything else. It runs while the admin renders, so it may call your own composables — your auth state, for example — and it must answer synchronously:

permission: ({ item }) => useUserSession().user.value?.role === 'admin' && !item?.published,

A refused action is shown disabled or hidden, per permissions.unauthorizedButtons.

permission decides which buttons the admin offers. What the handler's requests may do is still decided by nuxt-auto-api on the server.

The context

interface ActionContext {
  resource: string                                     // 'articles'
  refresh: () => Promise<void>                         // refetch every list and record of this resource
  toast: {
    success: (message: string) => void
    error: (message: string, error?: unknown) => void  // the API's message as the description
  }
  path: (...segments: Array<string | number>) => string // ctx.path(7) → '/api/articles/7' (honours the API prefix)
}

A handler that throws gets an error toast ("Publish failed" with the API's message); report your own errors with ctx.toast.error when you want a better one.

Next Steps

Need a Landing Page?

Modern landing pages with optional modules (blog, docs, forms, i18n). Let's discuss your project.

Build Your MVP

Full-stack SaaS development. Expert in database design, multi-tenancy, and scalable architecture.

Deployment Help

Dockerize your backend, set up CI/CD pipelines, deploy to Cloudflare or Hetzner. Early-stage setup.

Suggest a SaaS Tool

Missing a calculator or tool? Suggest what you'd like to see on our site.