Actions
Single record actions
You might need to give admin users a feature to perform some action on a single record. Actions can be displayed as buttons in the list view and/or in the three-dots menu.
Here's how to add a custom action:
{
resourceId: 'aparts',
options: {
actions: [
{
name: 'Auto submit', // Display name of the action
icon: 'flowbite:play-solid', // Icon to display (using Flowbite icons)
// Control who can see/use this action
allowed: ({ adminUser, standardAllowedActions }) => {
return true; // Allow everyone
},
// Handler function when action is triggered
action: async ({ recordId, adminUser }) => {
console.log("auto submit", recordId, adminUser);
return {
ok: true,
successMessage: "Auto submitted"
};
},
// Configure where the action appears
showIn: {
list: true, // Show in list view
showButton: true, // Show as a button
showThreeDotsMenu: true, // Show in three-dots menu
}
}
]
}
}
Action Configuration Options
name: Display name of the actionicon: Icon to show (using Flowbite icon set)allowed: Function to control access to the actionaction: Handler function that executes when action is triggeredshowIn: Controls where the action appearslist: whether to show in list viewshowButton: whether to show as a button on show viewshowThreeDotsMenu: when to show in the three-dots menu of show view
Access Control
You can control who can use an action through the allowed function. This function receives:
{
options: {
actions: [
{
name: 'Auto submit',
allowed: async ({ adminUser, standardAllowedActions }) => {
if (adminUser.dbUser.role !== 'superadmin') {
return false;
}
return true;
},
// ... other configuration
}
]
}
}
The allowed function receives:
adminUser: The current admin user objectstandardAllowedActions: Standard permissions for the current user
Return:
trueto allow accessfalseto deny access- A string with an error message to explain why access was denied
Here is how it looks:

You might want to allow only certain users to perform your custom bulk action.
To implement this limitation use allowed:
If you want to prohibit the use of bulk action for user, you can do it this way:
import { admin } from '../index';
....
bulkActions: [
{
label: 'Mark as listed',
icon: 'flowbite:eye-solid',
allowed: async ({ resource, adminUser, selectedIds }) => {
if (adminUser.dbUser.role !== 'superadmin') {
return false;
}
return true;
},
confirm: 'Are you sure you want to mark all selected apartments as listed?',
action: async ({ resource, selectedIds, adminUser, tr }) => {
const stmt = admin.resource('aparts').dataConnector.client.prepare(
`UPDATE apartments SET listed = 1 WHERE id IN (${selectedIds.map(() => '?').join(',')})`
);
await stmt.run(...selectedIds);
return { ok: true, message: tr(`Marked ${selectedIds.length} apartments as listed`) };
},
}
],
Action URL
Instead of defining an action handler, you can specify a url that the user will be redirected to when clicking the action button:
{
name: 'View details',
icon: 'flowbite:eye-solid',
url: '/resource/aparts', // URL to redirect to
showIn: {
list: true,
showButton: true,
showThreeDotsMenu: true,
}
}
The URL can be:
- A relative path within your admin panel (starting with '/')
- An absolute URL (starting with 'http://' or 'https://')
To open the URL in a new tab, add ?target=_blank to the URL:
{
name: 'View on Google',
icon: 'flowbite:external-link-solid',
url: 'https://google.com/search?q=apartment&target=_blank',
showIn: {
list: true,
showButton: true
}
}
☝️ Note: You cannot specify both
actionandurlfor the same action - only one should be used.
Custom bulk actions
You might need to give admin users a feature to perform same action on multiple records at once.
For example you might want allow setting listed field to false for multiple apartment records at once.
AdminForth by default provides a checkbox in first column of the list view for this purposes.
By default AdminForth provides only one bulk action delete which allows to delete multiple records at once
(if deletion for records available by resource.options.allowedActions)
To add custom bulk action quickly:
import { AdminUser } from 'adminforth';
import { admin } from '../index';
{
...
resourceId: 'aparts',
...
options: {
bulkActions: [
{
label: 'Mark as listed',
icon: 'flowbite:eye-solid',
// if optional `confirm` is provided, user will be asked to confirm action
confirm: 'Are you sure you want to mark all selected apartments as listed?',
action: async function ({selectedIds, adminUser }: {selectedIds: any[], adminUser: AdminUser }) {
const stmt = admin.resource('aparts').dataConnector.client.prepare(`UPDATE apartments SET listed = 1 WHERE id IN (${selectedIds.map(() => '?').join(',')})`);
await stmt.run(...selectedIds);
return { ok: true, error: false, successMessage: `Marked ${selectedIds.length} apartments as listed` };
},
}
],
}
}
Action code is called on the server side only and allowed to only authorized users.
☝️ AdminForth provides no way to update the data, it is your responsibility to manage the data by selectedIds. You can use any ORM system or write raw queries to update the data.
☝️ You can use
adminUserobject to check whether user is allowed to perform bulk action
Action response can return optional
successMessageproperty which will be shown to user after action is performed. If this property is not provided, no messages will be shown to user.
Here is how it looks:

Limiting access to bulk actions
You might want to allow only certain users to perform your custom bulk action.
To implement this limitation use allowed:
If you want to prohibit the use of bulk action for user, you can do it this way:
bulkActions: [
{
label: 'Mark as listed',
icon: 'flowbite:eye-solid',
allowed: async ({ resource, adminUser, selectedIds }) => {
if (adminUser.dbUser.role !== 'superadmin') {
return false;
}
return true;
},
// if optional `confirm` is provided, user will be asked to confirm action
confirm: 'Are you sure you want to mark all selected apartments as listed?',
action: async function ({selectedIds, adminUser }: {selectedIds: any[], adminUser: AdminUser }, allow) {
const stmt = admin.resource('aparts').dataConnector.client.prepare(`UPDATE apartments SET listed = 1 WHERE id IN (${selectedIds.map(() => '?').join(',')}`);
await stmt.run(...selectedIds);
return { ok: true, error: false, successMessage: `Marked ${selectedIds.length} apartments as listed` };
},
}
],
Custom Component
If you want to style an action's button/icon without changing its behavior, attach a custom UI wrapper via customComponent.
The file points to your SFC in the custom folder (alias @@/), and meta lets you pass lightweight styling options (e.g., border color, radius).
Below we wrap a “Mark as listed” action (see the original example in Custom bulk actions).
{
resourceId: 'aparts',
options: {
actions: [
{
name: 'Mark as listed',
icon: 'flowbite:eye-solid',
// UI wrapper for the built-in action button
customComponent: {
file: '@@/ActionBorder.vue', // SFC path in your custom folder
meta: { color: '#94a3b8', radius: 10 }
},
showIn: { list: true, showButton: true, showThreeDotsMenu: true },
action: async ({ recordId }) => {
await admin.resource('aparts').update(recordId, { listed: 1 });
return { ok: true, successMessage: 'Marked as listed' };
}
},
]
}
}
Use this minimal wrapper component to add a border/rounding around the default action UI while keeping the action logic intact.
Keep the <slot /> (that's where AdminForth renders the default button) and emit callAction (optionally with a payload) to trigger the handler when the wrapper is clicked.
<template>
<!-- Keep the slot: AdminForth renders the default action button/icon here -->
<!-- Emit `callAction` (optionally with a payload) to trigger the action when the wrapper is clicked -->
<!-- Example: provide `meta.extra` to send custom data. In list views we merge with `row` so recordId context is kept. -->
<div :style="styleObj" @click="emit('callAction', { ...props.row, ...(props.meta?.extra ?? {}) })">
<slot />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{
// meta can style the wrapper and optionally carry extra payload for the action
meta?: { color?: string; radius?: number; padding?: number; extra?: any };
// When used in list view, the table passes current row
row?: any;
// When used in show/edit views, the page passes current record
record?: any;
}>();
const emit = defineEmits<{ (e: 'callAction', payload?: any): void }>();
const styleObj = computed(() => ({
display: 'inline-block',
border: `1px solid ${props.meta?.color ?? '#e5e7eb'}`,
borderRadius: (props.meta?.radius ?? 8) + 'px',
padding: (props.meta?.padding ?? 2) + 'px',
}));
</script>
Pass dynamic values to the action
You can pass arbitrary data from your custom UI wrapper to the backend action by emitting callAction with a payload. That payload will be available on the server under the extra argument of your action handler.
Frontend examples:
<template>
<!-- Two buttons that pass different flags to the action -->
<button @click="emit('callAction', { asListed: true })" class="mr-2">Mark as listed</button>
<button @click="emit('callAction', { asListed: false })">Mark as unlisted</button>
<!-- Or keep the default slot button and wrap it: -->
<div @click="emit('callAction', { asListed: true })">
<slot />
</div>
</template>
<script setup lang="ts">
const emit = defineEmits<{ (e: 'callAction', payload?: any): void }>();
</script>
Backend handler: read the payload via extra.
{
resourceId: 'aparts',
options: {
actions: [
{
name: 'Toggle listed',
icon: 'flowbite:eye-solid',
showIn: { list: false, showButton: true, showThreeDotsMenu: true },
// The payload from emit('callAction', { asListed: true|false }) arrives here as `extra`
customComponent: {
file: '@@/ActionToggleListed.vue'
},
action: async ({ recordId, extra }) => {
const asListed = extra?.asListed === true;
// Example update (use your own data layer):
await admin.resource('aparts').update(recordId, { listed: asListed });
return { ok: true, successMessage: `Set listed=${asListed}` };
}
}
]
}
}
Notes:
- If you don’t emit a payload, the default behavior is used by the UI (e.g., in lists the current row context is used). When you do provide a payload, it will be forwarded to the backend as
extrafor your action handler. - You can combine default context with your own payload by merging before emitting, for example:
emit('callAction', { ...row, asListed: true })if your component has access to the row object.