User Tags is a label that behaves like a role to the whole of WordPress —it appears in $user->roles, answers current_user_can(), and WP_User_Query finds it— while granting no capability and never being written to the autoloaded {prefix}user_roles option. It is the tag engine that powers LeanRoles, released separately so any plugin can adopt it. It can also be activated as a plugin in its own right, with the admin screens switched on.
The problem
WordPress offers no primitive for saying «this user is a wholesale customer» without granting them permissions: there are only roles. That is why membership and commerce plugins keep inventing them. Every invented role is copied into an option that is unserialized and held in memory by every PHP worker on every request — and the sites that end up with forty of them are exactly the sites whose traffic is mostly logged in, where page caching never gets a look in. This library is the missing alternative.
Bundling it
Copy the directory into your plugin —git subtree, a submodule or a plain copy, whichever suits your release process— and include one file. That is the whole integration: do not require anything else and do not autoload src/. As with Action Scheduler, the newest copy present on the site is the one that boots.
require_once __DIR__ . '/libraries/user-tags/user-tags.php';
add_action( 'user_tags_ready', function () {
user_tags_register( 'wholesale', array( 'name' => 'Wholesale customer' ) );
} );
if ( function_exists( 'user_tags_add' ) ) {
user_tags_add( $user_id, 'wholesale' );
}Code language: PHP (php)
From that point the tag is indistinguishable from a role to everything else: current_user_can( 'wholesale' ), in_array( 'wholesale', $user->roles, true ) and get_users( array( 'role' => 'wholesale' ) ) all just work.
Optional admin interface
The screens ship with the library but load only if you ask for them, with a single filter added at include time:
add_filter( 'user_tags_enable_admin', '__return_true' );Code language: JavaScript (javascript)
That gets you Users → Tags to create and edit tags with CSV import and export, a column and filter links on the users list, bulk assign and remove, and checkboxes on the user profile. Nothing appears —or is even loaded from disk— until the filter says so, and the menu always sits under Users. Activated as a standalone plugin, these screens come switched on.
Where the data lives
Everything is stored in structures core already has: the catalogue in a user_tag taxonomy (outside autoload), per-tag config in termmeta, assignments in term_relationships (with an indexed term_taxonomy_id, so «everyone with tag X» is a seek) and a read mirror in usermeta that rides on the metadata cache WP_User_Query already primes, so the hot path costs no extra query. The taxonomy is the source of truth; the mirror is derived and rebuildable.
No custom tables, no dbDelta, no schema version, no migrations.
Built to coexist
It is deliberately shaped around avoiding the problems that Action Scheduler —the reference for a bundled WordPress library— accumulated over the years:
- No custom tables: it leans only on core structures, so there is no schema to migrate and nothing exotic left behind on uninstall.
- When several plugins bundle it,
user_tags_diagnostics()reports which copy is active, the file that registered it, and every other copy seen. - Version arbitration (
src/Versions.php) is frozen and its state lives in$GLOBALS, so a newer copy can read what an older one collected. - Feature detection —
user_tags_supports( 'user-query' )— instead of comparing version strings. - It forces no admin UI: optional screens ship with it but load only when a plugin opts in, and always sit under Users.