Install
$ agentstack add skill-lonsdale201-wp-agent-skills-je-listings-callback ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
JetEngine: register a Listings callback (Dynamic Field filter)
For developers extending JetEngine's Dynamic Field widget with custom value transforms — "Format date", "Format number", "Add URL scheme", "Convert units", "Pretty link", and so on. JetEngine ships ~30 built-ins; this skill is for the registration contract that lets your plugin add a new entry to the "Callback to filter field value" dropdown plus its companion controls.
Misconception this skill corrects
> "I'll register a callback by giving JE a string name and a class+method. JE will figure out how to call it."
JE will NOT figure it out. The callback identifier you pass in 'filter_callback' => [ 'unit_converter' ] and via register_callback( 'unit_converter', ... ) is the SAME STRING that JE later passes to call_user_func_array(). PHP's is_callable() is the gate at [callbacks.php:535](callbacks.php) — and is_callable( 'unit_converter' ) is false unless there is a global function with that name. A static method MyPlugin\Callbacks\UnitConverter::unit_converter() does NOT satisfy is_callable( 'unit_converter' ).
When the gate fails, apply_callback() returns null early ([callbacks.php:531-537](callbacks.php)) and the dynamic-field render assigns that null back to the value. The field renders as empty — no error, no warning, just a blank where the converted value should be. Easy to misdiagnose as "the callback ran and produced empty output" when in fact the gate rejected the callback and it never ran at all.
JE's own callbacks satisfy the gate two ways:
- PHP built-ins —
'number_format','wpautop','do_shortcode','human_time_diff','wp_oembed_get','make_clickable','wp_get_attachment_image','get_the_title','get_permalink','get_term_link','date_i18n','zeroise'. These are PHP/WP global functions, sois_callable()returns true. - JE-prefixed globals —
'jet_engine_date','jet_engine_proportional','jet_engine_get_user_data_by_id','jet_engine_url_scheme','jet_engine_render_multiselect','jet_engine_render_checklist', etc. These are declared as global functions in [includes/core/functions.php](functions.php) — NOT static methods. That is why they pass the callable gate.
Your callback MUST follow the same pattern: declare a global function (or use a fully-qualified static-method string Vendor\\My\\Callbacks\\Convert::run).
When to use this skill
Trigger this skill when ANY of the following is true:
- The user asks "how do I add a callback to the Dynamic Field widget", "Format custom values", "extend the JE field filter dropdown".
- The diff or file contains:
add_filter( 'jet-engine/listings/allowed-callbacks',add_filter( 'jet-engine/listings/allowed-callbacks-args',add_filter( 'jet-engine/listing/dynamic-field/callback-args',add_action( 'jet-engine/callbacks/register', or a class named*Callback*/*Converter*registering with JE. - A listing field set to "Filter field output" with a custom callback renders empty when it shouldn't — likely the callable-gate trap.
Workflow
1. Pick a registration path
JE supports two surfaces. They produce the same result; pick by ergonomics.
| Path | When to use | Hooks | |---|---|---| | Legacy 3-filter | Adding multiple callbacks at once, or matching the JE built-in style | jet-engine/listings/allowed-callbacks (label map), jet-engine/listings/allowed-callbacks-args (control schema), jet-engine/listing/dynamic-field/callback-args (positional args) | | Modern register_callback() | Adding one callback with self-contained args; less boilerplate | Action jet-engine/callbacks/register → $manager->register_callback( $name, $label, $args ) which fires the 3 filters internally |
The modern path is verified at [callbacks.php:35](callbacks.php) — the action fires inside the manager constructor. The register_callback() method at [callbacks.php:51](callbacks.php) stores the callback definition; register_callbacks() at line 66, register_callbacks_args() at line 81, and apply_callbacks_args() at line 117 then satisfy the same three filters that the legacy path uses directly.
2. Bootstrap
Both paths need function_exists( 'jet_engine' ) because the manager is what fires the action. Hook on plugins_loaded priority 11 (after JE's bootstrap):
add_action( 'plugins_loaded', static function (): void {
if ( ! class_exists( '\Jet_Engine' ) ) {
return;
}
// MODERN path
add_action( 'jet-engine/callbacks/register', [ \MyPlugin\Callbacks\Manager::class, 'register' ] );
// LEGACY path (alternative)
add_filter( 'jet-engine/listings/allowed-callbacks', [ \MyPlugin\Callbacks\Manager::class, 'add_callback_label' ] );
add_filter( 'jet-engine/listings/allowed-callbacks-args', [ \MyPlugin\Callbacks\Manager::class, 'add_callback_controls' ] );
add_filter( 'jet-engine/listing/dynamic-field/callback-args', [ \MyPlugin\Callbacks\Manager::class, 'apply_callback_args' ], 10, 4 );
}, 11 );
3. Declare the callback function as a GLOBAL function
This is the step everyone gets wrong. The callback identifier MUST be a PHP-callable string. The two acceptable shapes:
// Option A — global function. Readable, easy.
function myplugin_format_thousands_short( $value, int $decimals = 1, string $suffix_separator = '' ): string {
// implementation
}
// Option B — fully-qualified static method as a string. Verbose but namespaceable.
namespace MyPlugin\Callbacks;
class FormatThousandsShort {
public static function run( $value, int $decimals = 1, string $suffix_separator = '' ): string {
// implementation
}
}
// Register as the FQN string:
$manager->register_callback(
'MyPlugin\\Callbacks\\FormatThousandsShort::run',
__( 'Format thousands (1.5K)', 'myplugin' ),
[ /* args */ ]
);
Option A is what the JE built-ins use — see [functions.php:1226 (jet_engine_proportional)](functions.php), [line 1268 (jet_engine_date)](functions.php), [line 1279 (jet_engine_get_user_data_by_id)](functions.php). Mirror that style: prefix with your plugin slug (mvp_, myplugin_, je_skills_smoke_) to avoid clashes. Auto-loaded via the same loader as your classes.
4. Modern registration — register_callback()
namespace MyPlugin\Callbacks;
class Manager {
public static function register( $manager ): void {
$manager->register_callback(
'myplugin_format_thousands_short',
__( 'Format thousands (1.5K)', 'myplugin' ),
[
'thousands_decimals' => [
'label' => __( 'Decimal points', 'myplugin' ),
'type' => 'number',
'min' => 0,
'max' => 3,
'step' => 1,
'default' => 1,
],
'thousands_suffix_separator' => [
'label' => __( 'Separator before suffix', 'myplugin' ),
'type' => 'text',
'default' => '',
],
]
);
}
}
The args array entries become per-callback controls. JE adds the condition (dynamic_field_filter + filter_callback) entries automatically so the controls only show when this callback is selected ([callbacks.php:81-110](callbacks.php)). The control type accepts text / number / select / switcher, and select takes options.
5. Legacy registration — three filters by hand
namespace MyPlugin\Callbacks;
class Manager {
public static function add_callback_label( array $callbacks ): array {
$callbacks['myplugin_format_thousands_short'] = __( 'Format thousands (1.5K)', 'myplugin' );
return $callbacks;
}
public static function add_callback_controls( array $args ): array {
$args['thousands_decimals'] = [
'label' => __( 'Decimal points', 'myplugin' ),
'type' => 'number',
'min' => 0,
'max' => 3,
'step' => 1,
'default' => 1,
'condition' => [
'dynamic_field_filter' => 'yes',
'filter_callback' => [ 'myplugin_format_thousands_short' ],
],
];
$args['thousands_suffix_separator'] = [
'label' => __( 'Separator before suffix', 'myplugin' ),
'type' => 'text',
'default' => '',
'condition' => [
'dynamic_field_filter' => 'yes',
'filter_callback' => [ 'myplugin_format_thousands_short' ],
],
];
return $args;
}
public static function apply_callback_args( array $args, string $callback, array $settings = [], $widget = null ): array {
if ( 'myplugin_format_thousands_short' === $callback ) {
$args[] = $settings['thousands_decimals'] ?? 1;
$args[] = $settings['thousands_suffix_separator'] ?? '';
}
return $args;
}
}
The legacy path requires you to write the condition entries yourself; modern hides that. Otherwise identical.
6. Argument-flow: how settings reach your function
At runtime when the field renders:
- Dynamic Field widget calls
Jet_Engine_Listings_Callbacks::apply_callback( $input, $callback, $settings, $widget )([manager.php:535](manager.php)). apply_callback()runsis_callable($callback) && is_allowed_callback($callback)— the gate ([callbacks.php:535](callbacks.php)).- For built-in callbacks the
switchpopulates positional$argsdirectly. For your callback thedefaultcase firesapply_filters( 'jet-engine/listing/dynamic-field/callback-args', [ $result ], $callback, $settings, $widget )([callbacks.php:725-731](callbacks.php)) — the third filter orapply_callbacks_args()fromregister_callback(). Each entry of your$argsdefinition is appended in declaration order to the array, with the saved value or the declared default. call_user_func_array( $callback, $args )([callbacks.php:736](callbacks.php)) — the result becomes the rendered field value.
So your function signature MUST match: first parameter is the field value, then your declared args in the same order:
function myplugin_format_thousands_short(
$value, // = 1e9 ) { $short = $n / 1e9; $suffix = 'B'; }
elseif ( $abs >= 1e6 ) { $short = $n / 1e6; $suffix = 'M'; }
elseif ( $abs >= 1e3 ) { $short = $n / 1e3; $suffix = 'K'; }
else { return number_format( $n, 0 ); }
return number_format( $short, $decimals ) . $suffix_separator . $suffix;
}
Mismatched arg order is a silent-rendering bug — your function gets the wrong values but doesn't error.
Critical rules
- The callback identifier MUST satisfy
is_callable(). A bare static method name like'unit_converter'does not — only a global function name ('myplugin_unit_converter') or a fully-qualified static-method string ('MyPlugin\\Callbacks\\UnitConverter::run') does. The gate at [callbacks.php:535](callbacks.php) returnsnullon failure → field renders empty with no error. - Both paths produce the same result. The modern
register_callback()fires the legacy 3 filters internally — pick by ergonomics, not capability. - Namespace-prefix your callback identifier (
myplugin_*or fully-qualified) to avoid clashing with another plugin's same-named callback. - Function signature:
$valuefirst, then your args in declaration order. JE'sapply_callbacks_args()appends settings in the order you declared them ([callbacks.php:117-138](callbacks.php)). - Args declarations need a
condition. With the legacy path you write[ 'dynamic_field_filter' => 'yes', 'filter_callback' => [ 'your-callback' ] ]yourself. With the modern path JE adds it for you. Withoutcondition, your controls show under EVERY callback selection. - Each arg key is unique across the JE editor. The same args dictionary feeds every callback's UI. Conflicts (e.g. multiple plugins declaring
decimal_point) silently overwrite. Prefix arg keys:myplugin_decimal_pointnotdecimal_point. - Hook on
plugins_loaded:11so JE's listing manager is loaded. The actionjet-engine/callbacks/registerfires from theJet_Engine_Listings_Callbacksconstructor, called fromJet_Engine_Listings::__construct. - Return a SCALAR (string/number) from your function. The dynamic-field render at [render/dynamic-field.php:334-340](dynamic-field.php) accepts a value or a
WP_Error(which becomes a "callback applying" warning). Returning arrays/objects converts to "Array" / class-name strings.
Common mistakes
// WRONG — bare static method name. Fails is_callable() and the field renders empty.
class Convert {
public static function units( $value, $from, $to ) { /* ... */ }
}
add_filter( 'jet-engine/listings/allowed-callbacks', function( $cb ) {
$cb['convert_units'] = 'Convert units'; // 🔴 'convert_units' is not callable
return $cb;
} );
// Even if every condition lines up, JE's apply_callback() returns null. Field is empty.
// RIGHT — global function. The simplest fix.
function myplugin_convert_units( $value, $from = 'cm', $to = 'cm' ) { /* ... */ }
add_filter( 'jet-engine/listings/allowed-callbacks', function( $cb ) {
$cb['myplugin_convert_units'] = 'Convert units';
return $cb;
} );
// RIGHT — fully-qualified static method as a string. Less common but valid.
$manager->register_callback(
'MyPlugin\\Callbacks\\Convert::units',
__( 'Convert units', 'myplugin' ),
[ /* args */ ]
);
// WRONG — args declared without condition. Controls appear under every callback.
$args['my_decimal'] = [
'label' => 'Decimal',
'type' => 'number',
'default' => 2,
]; // 🔴 No condition → leaks into every callback's panel
// RIGHT — scope to your callback only.
$args['my_decimal'] = [
'label' => 'Decimal',
'type' => 'number',
'default' => 2,
'condition' => [
'dynamic_field_filter' => 'yes',
'filter_callback' => [ 'myplugin_format_thousands_short' ],
],
];
// WRONG — function signature in the wrong order.
function myplugin_format_thousands_short( int $decimals, $value ) { /* ... */ }
// 🔴 JE passes [ $value, $decimals ]. Your function receives $value as $decimals.
// Output is silently wrong — no error.
// RIGHT — $value first, then args in declaration order.
function myplugin_format_thousands_short( $value, int $decimals = 1, string $sep = '' ) { /* ... */ }
// WRONG — arg key collision with another plugin or a JE built-in.
$args['decimal_point'] = [ /* ... */ ]; // 🔴 conflicts with JE's number_format args
// RIGHT — prefix every arg key.
$args['myplugin_thousands_decimal_point'] = [ /* ... */ ];
// WRONG — returning a non-scalar from the callback.
function myplugin_get_meta_array( $value ) {
return [ 'a', 'b', 'c' ]; // 🔴 dynamic-field renders as "Array"
}
// RIGHT — return a string. Use a delimiter helper or implode for arrays.
function myplugin_get_meta_array( $value, string $delim = ', ' ) {
return implode( $delim, (array) $value );
}
Cross-references
- Run
je-query-builder-custom-typewhen the customization is "I need a new query type" rather than "I need a per-field transform". Different extension surface, complementary. - Run
je-dynamic-visibility-conditionwhen the customization is "show / hide this widget based on context" rather than "transform a field value". - Run
wp-plugin-architecturefor the companion-plugin scaffold and theplugins_loaded:11priority pattern.
What this skill does NOT cover
- Bricks-views and Gutenberg integration. The same callback registration drives Bricks Dynamic Field and JE's block-editor dynamic content; you don't register a separate callback per renderer. UI specifics for Bricks/Blocks are out of scope.
- **Ma
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: Lonsdale201
- Source: Lonsdale201/wp-agent-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.