Using Joomla Action Logs
Sooner or later, every website owner asks a question that starts with "who". Who unpublished the homepage last night? Who installed that extension nobody recognises? Did the new editor really delete twelve articles, or is the client mistaken?
Without a record, these questions have no answer, and you are left guessing.
Joomla answers them with a small but powerful core component called User Actions Log (com_actionlogs). It quietly records every important change on your site: saves, deletes, publishing, logins, installs, and configuration changes. Each record stores the user, the action, the time, and (optionally) the IP address. You do not need to install anything. It is already in your Joomla.
Who did what, when, and from where - recorded by Joomla without a single third-party extension.
This article explains how the Action Logs component works, what it records, how to read and filter the log, how notifications and CSV export work, and how to keep the log healthy on a busy site. The goal is to help website owners, administrators, and developers understand this quiet witness well enough to trust it the day they need it.
1. The Basics
1.1 What is the User Actions Log?
The User Actions Log (com_actionlogs) is Joomla's built-in audit-trail component. Joomla introduced it in version 3.9 (May 2018), and it has shipped in core ever since.
A few things make it special:
- It writes one row per recorded action into a single table:
#__action_logs. - It is driven by events, not polling. Every action emits a Joomla event, a plugin catches it, and a row is written.
- Each row stores the user, the timestamp, the extension context, the item id, the IP address, and a language-keyed message.
- It is read-only from the interface. Admins can view, filter, export, or purge the log, but they cannot edit individual rows.
That last point matters. An audit log you can edit is not really an audit log. Joomla deliberately offers no edit screen for log rows.
1.2 A Note on Naming: "Action Logs" vs "User Actions Log"
Before we go further, clear up one common source of confusion. Joomla uses two names for the same thing, and the inconsistency throws a lot of people off:
| Where you see it | Name used |
|---|---|
| Admin menu item (J4 / 5 / 6) | Users → User Actions Log |
| Help screen title | User Actions Log: Options |
| Toolbar heading on the list view | User Actions Log |
| Official docs site | User Actions Log |
| Component folder / internal name | com_actionlogs |
| Recorder plugin label | Action Log - Joomla |
| System plugin label | System - User Actions Log |
"User Actions Log" is the formal, original name Joomla gave the feature in 3.9. It still appears in the help docs and the toolbar heading. "Action Logs" is the shorter label people use in everyday speech; the admin menu itself says User Actions Log. They refer to the same component, the same database tables, and the same plugins. There is no separate "User Action Logs" feature.
A historical wrinkle adds to the confusion. In Joomla 3.9 and 3.10 the menu item lived under System → User Actions Log. From Joomla 4 onward it moved to Users → User Actions Log. Older tutorials and screenshots still show the old name and path. In this article "Action Logs" and "User Actions Log" mean the same thing; when in doubt, the canonical identifier is com_actionlogs.
1.3 Where Do I Find It?
In the Joomla 6 backend, these places are relevant:
Users → User Actions Log (the log viewer)
Users → User Actions Log → Options (settings)
Users → Manage → [a user] (per-user email alerts,
on the User Actions Log Options tab)
System → Manage → Plugins → Action Log - Joomla (the recorder)
System → Manage → Plugins → System - User Actions Log (the enabler)
The component lives at administrator/components/com_actionlogs/. The two plugins live at plugins/actionlog/joomla/ (the recorder) and plugins/system/actionlogs/ (the system plugin that loads it).
One note that trips people up: the component sits under Users, not under System. Some older screenshots suggest otherwise.
1.4 The Three Pieces
Action Logs is built from three parts that work together:
| Part | Role |
|---|---|
com_actionlogs |
Backend list view, filters, CSV export, purge. |
plg_actionlog_joomla |
Listens to events and writes the log rows. |
plg_system_actionlogs |
Loads the recorder, and adds the per-user email preferences to the user form. |
Disable the recorder and the log goes silent. Disable the system plugin and the log goes silent too, for the reason set out in 3.3 - it is not an optional extra.
1.5 Your First Look at the Log
Open Users → User Actions Log. The list is sorted newest first. A typical entry looks like this:
| Column | Example |
|---|---|
| Action | User article-editor logged in to administrator. |
| Date | 2026-05-28 09:14:02 (relative: "a few seconds ago") |
| Extension | com_users |
| User | admin |
| IP Address | 10.10.10.1. With IP Logging off - the default - the whole column is hidden. |
The action cell is rendered from the stored language key and its JSON, with links to the item and to the acting user. There is no expandable row: clicking follows those links. The raw JSON is only visible in the database, and we come back to it in section 6.
1.6 Filtering the List
The filter bar on the list view gives you four tools:
- Search: free text against the rendered message.
- User: narrow down to one user's activity.
- Extension: only
com_content, onlycom_users, and so on. - Date: a preset dropdown - Today, Past week, Past 1 month, Past 3 months, Past 6 months, Past year. There is no start and end date picker.
The most useful combination is User + Date. Because the date filter only offers presets, a question like "everything Alice did last Wednesday" means picking Past week and reading down the list, or querying the table directly (section 8.4).
Back to top2. What Gets Logged
2.1 The Default Loggable Extensions
You decide which components are watched under Options → Events To Log. This is a multi-select, and it ships with 21 core extensions enabled - not every one of them. Smart Search, Associations, Workflow, Privacy, Joomla Update and Action Logs itself are not in the default list:
com_banners com_cache com_categories com_checkin
com_config com_contact com_content com_fields
com_guidedtours com_installer com_media com_menus
com_messages com_modules com_newsfeeds com_plugins
com_redirect com_scheduler com_tags com_templates
com_users
Remove an extension from this list and its events are silently dropped. No rows are written for it. Trimming this list is the single most effective way to reduce log noise on a busy site.
2.2 Content Lifecycle Events
For every loggable component, the recorder plugin subscribes to three content events:
| Event | What it logs |
|---|---|
onContentAfterSave |
Create or update, with id, title, and a new/updated flag. |
onContentAfterDelete |
Delete, with id and title (captured before deletion). |
onContentChangeState |
Publish, unpublish, archive, or trash, with the new state. |
So every article you save, every category you trash, and every menu item you publish writes one row.
2.3 User Lifecycle Events
User events are the most important for spotting account abuse:
| Event | What it logs |
|---|---|
onUserAfterLogin |
Successful login (frontend or backend, but never API logins). |
onUserLoginFailure |
Failed login, but only if the username exists. |
onUserLogout |
Explicit logout. |
onUserAfterSave |
Create or edit a user (also captures group changes). |
onUserAfterDelete |
Delete a user. |
onUserAfterRemind |
"Forgot username" requested. |
onUserAfterResetRequest |
"Forgot password" link requested. |
onUserAfterResetComplete |
Password reset finished. |
One detail surprises people: failed logins with an unknown username are not logged. The plugin checks for an existing user first, which keeps automated scanner traffic out of the audit trail.
2.4 Extension Lifecycle Events
Anything that touches the extensions table is recorded:
| Event | What it logs |
|---|---|
onExtensionAfterInstall |
Install: type, name, version. |
onExtensionAfterUpdate |
Update: old version to new version. |
onExtensionAfterUninstall |
Uninstall. |
onExtensionAfterSave |
Edit, for example enable or disable a plugin. |
onJoomlaAfterUpdate |
Core Joomla version bump. |
This is pure gold for forensics after an incident. The question "when did we install that vulnerable plugin?" becomes one query away.
2.5 Configuration Changes
Joomla also records configuration saves:
onApplicationAfterSave: Global Configuration was saved.onExtensionAfterSavewith contextcom_config.component: a per-component Options screen was saved.
Both record only that the settings were saved, plus who did it - not which fieldset changed, and not the before and after values. If you need the exact diff, that takes a small custom plugin (see section 11).
2.6 Operational Events
A few housekeeping actions are logged too:
| Event | What it logs |
|---|---|
onAfterCheckin |
An admin force-checked-in a locked record. |
onAfterPurge |
The cache was cleared. |
onAfterLogPurge |
The action log itself was purged. |
onAfterLogExport |
A CSV export was downloaded. |
Yes, purging the log is itself logged. After a purge, you are left with exactly one row: "User admin purged the action logs." The audit trail is self-documenting.
2.7 API Requests (optional)
If you set Options → Log API Requests = Yes, Joomla records every Web Services API call as it dispatches. A second option, Request Methods To Log, lets you choose which methods to capture. It defaults to GET only, but most production sites that enable this want POST, PUT, PATCH, and DELETE at minimum.
A warning: API logging fills the table fast. Turn it on for an investigation, then turn it off for steady-state operation. See section 8 for the numbers.
Back to top3. Notifications
3.1 The Opt-in Model
Each user can subscribe to email alerts for one or more extensions on their own account, under Users → Manage → [the user] → User Actions Log Options. The two fields are Email Notifications and Events to Email. The preferences live in #__action_logs_users:
user_id notify (0/1) extensions (JSON array, e.g. ["com_users","com_installer"])
- A user who is not in this table receives no notifications.
- A user with
notify = 1andextensions = com_users,com_installergets an email every time someone touches users or extensions.
Notifications are a per-user choice, not a site-wide setting. Power users and security officers opt in; everyone else stays quiet.
3.2 What the Email Looks Like
The notifier renders the same human-readable message you see in the list view, plus:
- A direct link to the affected item, where one applies.
- The acting user's name.
The mail uses the system mailer, so check it is configured under System → Global Configuration → Server → Mail before relying on notifications. On a local Docker stack, a tool like MailHog catches every outgoing message so you can test safely.
3.3 The System Plugin's Role
This one is worth getting right, because the obvious reading is the wrong way round. The system plugin is not a notifier sitting on top of a logger. It is what makes the logger run at all.
Joomla only loads a plugin group when something asks for it, and the actionlog group is asked for in exactly one place - plg_system_actionlogs, on every request:
public function onAfterInitialise(AfterInitialiseEvent $event): void
{
// Import actionlog plugin group so that these plugins will be triggered for events
PluginHelper::importPlugin('actionlog', null, true, $event->getApplication()->getDispatcher());
}
So the division of labour is:
- Action Log - Joomla subscribes to the content, user and extension events and writes the rows - but only once the system plugin has loaded it.
- System - User Actions Log loads that group, adds the User Actions Log Options tab to the user edit form (the Email Notifications and Events to Email fields), and keeps
#__action_logs_usersin step when users are saved or deleted. - com_actionlogs, the component, does the rest:
ActionlogModel::addLog()writes each row and then callssendNotificationEmails(), andActionlogsHelper::getHumanReadableLogMessage()turns a stored language key plus its JSON into the sentence you read in the list.
The practical consequence is the opposite of what you might expect: disabling the system plugin stops logging entirely. The recorder is never loaded, so no rows are written, and with no new rows there are no emails either. It does not leave you with a growing log and a quiet mailbox.
If you want to silence email blasts during a bulk migration and keep the audit trail, do not touch the plugins. Clear Events to Email on the accounts that subscribe, or set Email Notifications to No for the duration.
Back to top4. Export and Purge
4.1 CSV Export
The list view has two export buttons in the toolbar:
| Button | What it exports |
|---|---|
| Export Selected as CSV | Only the rows you checked. |
| Export All as CSV | The entire log, respecting the active filters. |
The delimiter is configurable under Options → CSV Delimiter. Choose a semicolon (;) if your spreadsheet locale (for example Dutch Excel) treats commas as decimal separators.
The export is streamed one row at a time, so even a log with a million rows will not run the PHP worker out of memory.
4.2 CSV Injection Protection
Joomla escapes any cell that starts with =, +, - or @ by prepending a space. This blocks the classic "Excel formula injection" attack, where a logged username like =cmd|'/c calc'!A1 would otherwise run as a formula when an admin opens the CSV.
If you ever write a custom exporter, keep that escaping. It is there for a good reason.
4.3 Purging the Log
The Clear Log button in the toolbar wipes every row from #__action_logs. (The separate Delete button removes only the rows you have checked.) The purge then writes one row of its own, so afterwards you have a single entry recording who purged the log and when.
Common reasons to purge:
- After a long migration period, when you want a clean slate.
- Before exporting only fresh activity.
There is no undo. For automatic clean-up, core ships a Delete ActionLogs scheduled task (section 11.3).
Back to top5. IP Logging and Privacy
5.1 The Default: IP Logging Off
Out of the box, Options → IP Logging = No. When IP logging is off, every row stores a placeholder instead of a real IP, and the list view shows "Disabled".
When you turn it on, Joomla reads the visitor's IP and honours the X-Forwarded-For and Client-Ip headers. This is a single on/off switch, Behind Load Balancer (default No) under Global Configuration → Server → Proxy; core keeps no trusted-proxy list. If the resolved value is not a valid IP, the row records "Invalid" instead.
5.2 GDPR Considerations
An IP address is personal data under GDPR. Turning IP logging on means you should:
- Have a lawful basis. Legitimate interest for security is usually fine.
- Mention IP logging in your privacy policy.
- Define a retention policy. IPs kept forever are hard to justify.
Reasonable defaults for a public Joomla site:
- IP logging on: you will want it the day you get hacked.
- Retention of about 90 days: purge older rows with a Scheduler task.
- A line in the privacy policy under "Security logs".
5.3 Right-to-be-forgotten Requests
When a user asks to be deleted, you do not have to destroy the whole audit trail. Anonymise instead:
- Delete the user through
com_users. This writes one final log row that references the deleted account by id. - Run an SQL update to scrub the historical rows:
UPDATE jos_action_logs
SET user_id = 0,
ip_address = '0.0.0.0',
message = REPLACE(message, 'OldUsername', 'deleted-user')
WHERE user_id = 123;
The audit trail stays intact; only the personally identifying parts are removed. Replace jos_ with your actual database prefix.
6. Under the Hood
6.1 The Four Tables
Action Logs uses four database tables:
#__action_logs one row per logged action
#__action_logs_extensions the list of extensions that participate
#__action_log_config how each content type renders
#__action_logs_users per-user notification preferences
The main one is #__action_logs. The other three are small reference tables.
6.2 The Main Log Table
The core fields of #__action_logs are:
id auto-increment primary key
message_language_key the template key, e.g. PLG_SYSTEM_ACTIONLOGS_CONTENT_UPDATED
message JSON with the placeholder values
log_date when the action happened
extension context, e.g. com_content.article
user_id acting user (0 = guest or system)
item_id affected item id (0 if not applicable)
ip_address real IP, "Disabled", or "Invalid"
The table carries several indexes on user_id, (user_id, log_date), (user_id, extension), and (extension, item_id). These are what keep the "show all actions for user X" filter fast, even on a log with millions of rows.
6.3 The Message JSON
This is the clever part. The message column does not hold the rendered text. It holds a JSON object of placeholders:
{
"action": "update",
"type": "PLG_ACTIONLOG_JOOMLA_TYPE_ARTICLE",
"id": 42,
"title": "About Us",
"itemlink": "index.php?option=com_content&task=article.edit&id=42",
"userid": 100,
"username": "admin",
"accountlink": "index.php?option=com_users&task=user.edit&id=100"
}
The message_language_key is the template, for example:
"User {username} updated {type} {title}."
At display time, Joomla substitutes the JSON values into the template and wraps the user name and the title in links. This is exactly why the log is translatable: the message is a key, not a fixed string. Install the Dutch language pack and "User admin updated..." becomes "Gebruiker admin werkte ... bij."
6.4 The Content-type Map
The table #__action_log_config tells the renderer how to read each content type. For each type it stores which column holds the id, which holds the title, the table name, and a text prefix used to build the language key:
type_alias id_holder title_holder table_name
com_content.article id title #__content
com_categories.category id title #__categories
com_redirect.link id old_url #__redirect_links
com_modules.module id title #__modules
...about 23 rows in Joomla 6
A custom component can register its own type by inserting a row here. That is the hook developers use to make their own extensions log-aware (section 11).
Back to top7. Permissions and Access
7.1 There Is No Per-Component ACL
This section used to describe an Options → Permissions tab and a view-only "audit officer" group. Neither exists. It is worth stating plainly, because the pattern is appealing and people do go looking for it.
com_actionlogs ships no access.xml - 23 other core components do - and its config.xml declares a single fieldset, actionlogs, holding the six logging options and nothing else. The component Options screen is built only from that file, so there is no Permissions tab to open and no per-component actions to grant or withhold.
Access is decided in one place instead, and it is blunt:
// administrator/components/com_actionlogs/src/Dispatcher/Dispatcher.php
protected function checkAccess()
{
$user = $this->app->getIdentity();
// Access check
if (!$user->authorise('core.admin')) {
throw new NotAllowed(..., 403);
}
}
That is site-wide core.admin, the Super User permission. Anyone who cannot pass it gets a 403 on the whole component.
7.2 What This Means in Practice
The consequences are worth spelling out, because they cut against how most people expect an audit log to work:
- Only Super Users can read the log. There is no way to grant a manager, a security officer or an auditor read access to Action Logs through the Joomla interface.
- There is no view-only mode. Everyone who can read the log can also clear it. The toolbar offers Delete for the selected rows and Clear Log for all of them, and nothing gates one differently from the other.
- Everyone who can read it can also erase their own tracks - not with raw SQL, but with two clicks in the list view.
So Action Logs is an audit trail for a site where you trust every Super User. It is not a control that constrains them. If you need separation of duties - someone who can review activity but not alter the record, or a Super User whose actions are recorded somewhere they cannot reach - you need that outside Joomla: ship the rows to an external log collector, replicate the table, or take database backups an administrator cannot delete.
What the component does get right is the shape of the record itself. There is no Create and no Edit anywhere in the interface: you cannot add a row by hand and you cannot alter one. Rows are written by the recorder or not at all.
Back to top8. Performance and Scale
8.1 How Much Does It Grow?
For a normal content team, the log is tiny. A team of five editors might generate:
~50 article saves/day + ~20 menu/module saves/day + ~10 logins/day
= roughly 80 rows/day = ~2,400 rows/month = ~30,000 rows/year
Each row is around 500 bytes, so one year is about 15 MB. No problem at all.
API logging changes everything. A site serving 10 requests per second to its API would hit 864,000 rows per day. Do not enable it casually.
8.2 Pruning Strategies
For a plain age-based policy, use the core Delete ActionLogs task (section 11.3) - it does exactly this and needs no SQL. What it cannot do is discriminate by extension, so a selective policy still means SQL, run by hand during maintenance or from a task plugin of your own. A simple prune deletes everything older than 90 days:
DELETE FROM jos_action_logs
WHERE log_date < NOW() - INTERVAL 90 DAY;
For a smarter policy, keep the security-relevant events longer and prune only the routine content events - this is the case the core task does not cover:
DELETE FROM jos_action_logs
WHERE log_date < NOW() - INTERVAL 90 DAY
AND extension NOT IN ('com_users', 'com_installer', 'com_config');
Logins, installs, and config changes are worth keeping longer than ordinary saves. Their forensic value grows over time.
8.3 Fast Queries vs Slow Queries
Because of the indexes, queries that filter on user and date, or on extension and item, are fast:
-- Fast: all actions by one user in the last 7 days
SELECT * FROM jos_action_logs
WHERE user_id = 100 AND log_date > NOW() - INTERVAL 7 DAY
ORDER BY log_date DESC;
Free-text search through the JSON message has no usable index and scans the whole table:
-- Slow: free-text search through the message
SELECT * FROM jos_action_logs
WHERE message LIKE '%phishing%';
The list view's search box is exactly this kind of LIKE '%...%' scan. It is fine for the occasional admin lookup, but do not build scripted polling on top of it.
8.4 Triage SQL You Will Use Often
Yesterday's failed logins, the brute-force canary:
SELECT user_id, COUNT(*) AS attempts
FROM jos_action_logs
WHERE message_language_key = 'PLG_ACTIONLOG_JOOMLA_USER_LOGIN_FAILED'
AND log_date > NOW() - INTERVAL 1 DAY
GROUP BY user_id
HAVING attempts > 5
ORDER BY attempts DESC;
Recent extension installs, a supply-chain alarm:
SELECT log_date, user_id, message
FROM jos_action_logs
WHERE extension = 'com_installer'
AND log_date > NOW() - INTERVAL 7 DAY
ORDER BY log_date DESC;
Remember to replace jos_ with your real database prefix.
9. Common Mistakes and Pitfalls
9.1 "Nothing is being logged"
Work through this checklist, in order:
- Is the Action Log - Joomla plugin enabled?
- Is the affected component in the Logged Extensions list?
- Did the action actually succeed? The recorder hooks the "after" events, so a save that fails validation never reaches it.
- Are you looking at the right time range in the filter?
9.2 "Failed logins are not all logged"
This is intentional. The plugin ignores failed logins for unknown usernames so that automated scanners do not flood the table. Only failures for an existing username get a row.
9.3 "Article saves are logged twice"
They are not. The recorder does subscribe to onContentAfterSave for both the backend edit screen and the frontend submit form, but it aliases one to the other before writing:
protected $contextAliases = ['com_content.form' => 'com_content.article'];
So a front-end submission produces one row, recorded against com_content.article. If you really are seeing two rows for one save, look for a second extension subscribing to the same event rather than assuming core does it.
9.4 "The user shown is not who did it"
The recorder logs the currently authenticated user at the moment the event fires. CLI scripts and Scheduler tasks run as the guest user, so they appear with user_id = 0. A user_id = 0 usually means one of three things:
- A genuinely unauthenticated frontend event, such as a forgot-password request.
- A scheduled task or CLI command.
- A custom plugin running outside an HTTP request.
The extension column, and the IP address if logging is on, usually tell the cases apart.
9.5 "The message says 'User undefined updated ...'"
That is a missing language key. It happens when a custom extension fires an event but does not ship the matching PLG_ACTIONLOG_... strings, or when the active language pack lacks the key. The fix is to add the key to the language file or register the prefix in #__action_log_config.
9.6 "Purge wiped my evidence"
There is no undo. Before you ever purge:
- Export to CSV first (toolbar, Export All as CSV).
- Archive the CSV outside the Joomla install.
- Then purge.
For any site under a compliance regime, the rule is simple: never purge without an exported copy and a record of who did it.
9.7 "I cannot find Action Logs in the menu"
It is under Users, not System. And the component refuses anyone without site-wide core.admin, so a non-Super-User account will not see the link and gets a 403 if it navigates there directly (section 7.1).
10. Security and Compliance
10.1 The Audit Trail Is Itself a Target
An attacker who gains admin access often wants to do something malicious and then hide the evidence. The action log makes hiding harder, but it is not bulletproof. A Super User can clear the log, delete selected rows, or disable the recorder - all from the interface, no database access required. And since only Super Users can open the component at all, the people the log would most usefully constrain are exactly the people who can edit it.
Sensible mitigations:
- Restrict the Delete permission to a single dedicated account.
- Stream the log offsite so a local copy alone cannot be the only record.
- Watch for the meta-events: a purge, or the recorder plugin being disabled.
Trust but verify applies to your own admins. The log is a deterrent and a forensic tool, not a guarantee.
10.2 Turning Logs Into Action
A log you only read after the fact gives you half its value. Action logs can also feed automated responses:
- Brute-force alerting: a Scheduler task runs the failed-login query and pushes an alert to Slack or email when attempts cross a threshold.
- Suspicious-install pager: a webhook on extension installs that pings the security team. An extension installed at 03:00 by a normally 9-to-5 admin is a red flag.
- Privilege-escalation watch: alert when anyone is added to the Super Users group. This should almost never happen.
The action log is your detection layer. Pair it with a response layer, either inside Joomla via Scheduler tasks and plugins, or wired to an external monitoring system.
10.3 The Minimum-viable Security Setup
For a production site you actually care about, about twenty minutes of setup buys you a lot:
- Action Logs enabled for
com_users,com_installer,com_config,com_plugins,com_modules, andcom_templates. - IP logging on, with a 90-day retention task.
- Notifications on for super-user accounts, watching
com_usersandcom_installer. - The Delete permission restricted to a single dedicated security account.
- A daily look at the failed-login query, or its dashboard equivalent.
It pays off the first time someone tries to be clever.
Back to top11. Customisation for Developers
11.1 Registering a Custom Component for Logging
You can make your own component log-aware in two steps, without writing a plugin. First, add it to the loggable list:
INSERT INTO jos_action_logs_extensions (extension)
VALUES ('com_mycomponent');
Second, add content-type rows so the renderer knows how to format the messages:
INSERT INTO jos_action_log_config
(type_title, type_alias, id_holder, title_holder, table_name, text_prefix)
VALUES
('myitem', 'com_mycomponent.item', 'id', 'name',
'#__mycomponent_items', 'PLG_ACTIONLOG_MYCOMPONENT');
Then ship language strings that start with PLG_ACTIONLOG_MYCOMPONENT_... for each action. As long as your component emits the standard onContentAfterSave and similar events with the right context, the existing recorder handles the rest.
11.2 Logging From Your Own Code
For a one-off log entry from a controller, model, or import script:
$model = $this->app->bootComponent('com_actionlogs')
->getMVCFactory()
->createModel('Actionlog', 'Administrator');
$model->addLog(
[['action' => 'custom', 'detail' => 'Manual entry from import script']],
'COM_MYCOMPONENT_LOG_IMPORT_RAN',
'com_mycomponent.import',
$userId // 0 = current user
);
This is handy for batch scripts that need to leave a footprint in the audit trail without going through the event system.
11.3 Scheduled Pruning
Core ships a retention task for exactly this. You do not need a raw SQL job, and the "SQL Operations" task type that older advice points at does not exist in Joomla 6 at all.
Enable the Task - Delete Action Logs plugin, then go to System → Manage → Scheduled Tasks → New and pick Delete ActionLogs:
| Field | Value |
|---|---|
| Task type | Delete ActionLogs ("Delete Action logs after specified days.") |
| Title | Anything you like, for example Prune action logs |
| Days to delete action logs after | logDeletePeriod, default 7, minimum 1. Set it to your retention window - 90 for a quarter, 365 for a year. |
| Rule | Cron-style: 0 3 * * * (every night at 03:00) |
The task deletes from #__action_logs where log_date is older than the window you set. Nothing else is touched: the notification preferences in #__action_logs_users and the extension list in #__action_log_config stay as they are.
Watch the default. At 7 days a task created without changing that field will throw away almost everything, which is the opposite of what most people set a retention task up for. Decide the number before you save.
For the most reliable timing, trigger the Scheduler from a real system cron job rather than relying on website traffic.
Back to top12. Action Logs vs Other Logs
12.1 Action Logs vs the System Log
Joomla also has a low-level system log written by Log::add(). The two serve different goals:
| Aspect | Action Logs | System log |
|---|---|---|
| Editor-friendly UI | Yes | No, text files |
| Stored in | Database | Files |
| Translatable messages | Yes | No |
| Per-user notifications | Yes | No |
| Best for | Editorial audit (who did what) | Technical events (errors, performance) |
Use Action Logs for "who did what in the CMS" and the system log for technical events. They complement each other.
12.2 Action Logs vs Server Logs
Action logs live at the application layer. They are blind to anything that never reaches the Joomla event system. For a full picture you also need the infrastructure layer:
| Layer | What it sees | What it misses |
|---|---|---|
| Action log | CRUD actions, logins, installs, config changes: who and what | Requests that did not dispatch, PHP fatals, server-level blocks |
| Apache / nginx access log | Every HTTP request: URL, status, IP, user-agent | The Joomla user, the affected item, the intent |
| PHP error log | Fatals, warnings, stack traces | Successful actions, user identity |
An example shows the difference. A 500-error on an article shows up in the PHP error log and the access log, but not in the action log, because the save event never fired. The opposite is also true: an editor quietly trashing a page shows up in the action log, but in the access log it is just one more POST with status 200.
For real forensic work you correlate timestamps across all of them. Action logs answer who; server logs answer how the request landed; together they tell you what actually happened.
Back to top13. Best Practices
If you remember only a few things from this article, remember these:
- Keep both Action Logs plugins enabled in production.
- Trim the Logged Extensions list to cut noise on busy sites.
- Turn on IP logging, but pair it with a retention task and a privacy-policy line.
- Never purge without an exported CSV stored safely outside Joomla.
- Restrict the Delete permission to one dedicated account.
- Leave API logging off unless you are actively investigating something.
14. Quick Reference
COMPONENT Users → User Actions Log
NOTIFICATIONS Users → Manage → [user] → User Actions Log Options
OPTIONS Users → User Actions Log → Options
RECORDER System → Plugins → Action Log - Joomla
NOTIFIER System → Plugins → System - User Actions Log
MAIN TABLE #__action_logs
LOGGED EXTS Options → Logged Extensions (21 by default)
IP LOGGING Options → IP Logging (OFF by default)
API LOGGING Options → Log API Endpoints (OFF by default)
EXPORT Toolbar → Export All as CSV / Export Selected as CSV
PURGE Toolbar → Purge (no undo, self-logging)
PRUNE Scheduled Tasks → Delete ActionLogs (Task - Delete Action Logs)
SINCE Joomla 3.9 (May 2018)
Back to top15. Summary
The User Actions Log is one of those Joomla features that asks for nothing and gives a lot. It sits quietly in the background, recording every meaningful change, and the first time a client asks "who unpublished the homepage?" it earns its keep many times over.
For a few minutes of setup you get:
- Out-of-the-box auditing for 21 core components, with no extra extensions.
- Translated, human-readable messages built from JSON placeholders.
- Per-user notifications with opt-in granularity.
- CSV export with injection protection for safe review in spreadsheets.
- Optional IP logging that respects privacy by default.
- An indexed schema that scales comfortably to millions of rows.
It is not perfect. It is not tamper-evident on its own, and a determined super-user can still edit the database directly. But as a detection layer, paired with sensible permissions and a response plan, it turns "we have no idea what happened" into "here is exactly who did what, and when". If you run a Joomla site you care about, switch it on, tune it, and learn to read it before you need it.
Back to top

Peter is a Joomla specialist and a Linux admin for fast, secure and scalable websites.













