je-listings-callback
Register a custom Listings callback for JetEngine — the per-field transform fired when a Dynamic Field widget renders meta values (Format date, Format number, Get post title, Convert units, …). Two registration paths are supported. Legacy 3-filter via jet-engine/listings/allowed-callbacks (label), jet-engine/listings/allowed-callbacks-args (control schema), and jet-engine/listing/dynamic-field/callback-args (positional args at runtime). Modern single-call $manager->register_callback($name, $label, $args) hooked on jet-engine/callbacks/register fires the three filters internally for you. Critical contract — the callback identifier you register MUST be a real PHP callable string (a global function name OR `Fully\\Qualified\\Class::method`). Bare static method names like `'unit_converter'` fail JE's is_callable() gate at apply_callback() and the rendered field silently becomes empty. Use when scaffolding a JetEngine companion plugin's listing-field transform, when seeing "callback appl...
Best use case
je-listings-callback is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Register a custom Listings callback for JetEngine — the per-field transform fired when a Dynamic Field widget renders meta values (Format date, Format number, Get post title, Convert units, …). Two registration paths are supported. Legacy 3-filter via jet-engine/listings/allowed-callbacks (label), jet-engine/listings/allowed-callbacks-args (control schema), and jet-engine/listing/dynamic-field/callback-args (positional args at runtime). Modern single-call $manager->register_callback($name, $label, $args) hooked on jet-engine/callbacks/register fires the three filters internally for you. Critical contract — the callback identifier you register MUST be a real PHP callable string (a global function name OR `Fully\\Qualified\\Class::method`). Bare static method names like `'unit_converter'` fail JE's is_callable() gate at apply_callback() and the rendered field silently becomes empty. Use when scaffolding a JetEngine companion plugin's listing-field transform, when seeing "callback appl...
Teams using je-listings-callback should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/je-listings-callback/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How je-listings-callback Compares
| Feature / Agent | je-listings-callback | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Register a custom Listings callback for JetEngine — the per-field transform fired when a Dynamic Field widget renders meta values (Format date, Format number, Get post title, Convert units, …). Two registration paths are supported. Legacy 3-filter via jet-engine/listings/allowed-callbacks (label), jet-engine/listings/allowed-callbacks-args (control schema), and jet-engine/listing/dynamic-field/callback-args (positional args at runtime). Modern single-call $manager->register_callback($name, $label, $args) hooked on jet-engine/callbacks/register fires the three filters internally for you. Critical contract — the callback identifier you register MUST be a real PHP callable string (a global function name OR `Fully\\Qualified\\Class::method`). Bare static method names like `'unit_converter'` fail JE's is_callable() gate at apply_callback() and the rendered field silently becomes empty. Use when scaffolding a JetEngine companion plugin's listing-field transform, when seeing "callback appl...
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# 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, so `is_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):
```php
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:
```php
// 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()`
```php
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
```php
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:
1. Dynamic Field widget calls `Jet_Engine_Listings_Callbacks::apply_callback( $input, $callback, $settings, $widget )` ([manager.php:535](manager.php)).
2. `apply_callback()` runs `is_callable($callback) && is_allowed_callback($callback)` — the gate ([callbacks.php:535](callbacks.php)).
3. For built-in callbacks the `switch` populates positional `$args` directly. For your callback the `default` case fires `apply_filters( 'jet-engine/listing/dynamic-field/callback-args', [ $result ], $callback, $settings, $widget )` ([callbacks.php:725-731](callbacks.php)) — the third filter or `apply_callbacks_args()` from `register_callback()`. Each entry of your `$args` definition is appended in declaration order to the array, with the saved value or the declared default.
4. `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:
```php
function myplugin_format_thousands_short(
$value, // <- index 0: raw field value (string|int|float)
int $decimals = 1, // <- index 1: thousands_decimals
string $suffix_separator // <- index 2: thousands_suffix_separator
): string {
if ( ! is_numeric( $value ) ) { return (string) $value; }
$n = (float) $value;
$abs = abs( $n );
if ( $abs >= 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) returns `null` on 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: `$value` first, then your args in declaration order.** JE's `apply_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. Without `condition`, 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_point` not `decimal_point`.
- **Hook on `plugins_loaded:11`** so JE's listing manager is loaded. The action `jet-engine/callbacks/register` fires from the `Jet_Engine_Listings_Callbacks` constructor, called from `Jet_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
```php
// 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-type`** when 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-condition`** when the customization is "show / hide this widget based on context" rather than "transform a field value".
- Run **`wp-plugin-architecture`** for the companion-plugin scaffold and the `plugins_loaded:11` priority 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.
- **Macros (`%macro%`).** Different system from callbacks. Macros resolve dynamic placeholders inside text fields; callbacks transform the final value. Use the JE Macros API for that, not this.
- **Listing item layout / template.** Callbacks change a field's RENDERED VALUE; layout and which fields appear belong to the listing template configuration.
- **Performance.** A callback runs per field per item per render. Heavy work (DB queries, REST calls) should cache per-request manually (e.g. static array property on a helper class).
- **Editor-side preview.** The field-filter UI in the listing-editor preview is JE-managed; you can't add custom Vue controls beyond `text` / `number` / `select` / `switcher`.
## References
- Callback manager: [wp-content/plugins/jet-engine/includes/components/listings/callbacks.php:14](callbacks.php) — `Jet_Engine_Listings_Callbacks`. Constructor at line 23 fires `jet-engine/callbacks/register` at line 35. `register_callback()` at line 51, `register_callbacks()` at 66, `register_callbacks_args()` at 81, `apply_callbacks_args()` at 117. Built-in label map at `get_cllbacks_for_options()` line 145. Built-in args dictionary at `get_callbacks_args()` line 187. The dispatcher with the `is_callable()` gate at `apply_callback()` line 529, `call_user_func_array()` at line 736.
- Field-render entry point: [includes/components/listings/render/dynamic-field.php:328](dynamic-field.php) — `render_filtered_result()` and `apply_callback()` wrapper at line 397.
- Built-in JE callback functions: [includes/core/functions.php](functions.php) — `jet_engine_proportional()` at line 1226, `jet_engine_date()` at line 1268, `jet_engine_get_user_data_by_id()` at line 1279. All are GLOBAL functions, which is what makes them satisfy `is_callable()`.
- Crocoblock developer documentation: <https://github.com/Crocoblock/developer-documentation/tree/main/01-jet-engine>.Related Skills
webapp-testing
Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.
web-artifacts-builder
Suite of tools for creating elaborate, multi-component claude.ai HTML artifacts using modern frontend web technologies (React, Tailwind CSS, shadcn/ui). Use for complex artifacts requiring state management, routing, or shadcn/ui components - not for simple single-file HTML/JSX artifacts.
valyu
Search the live web and 36+ specialised data sources including SEC filings, PubMed, ChEMBL, clinical trials, FRED economic indicators, and patent databases. Use when current, authoritative, or paywalled data is required.
ui-ux-pro-max
AI-powered design intelligence with 67 UI styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 15+ tech stacks. Generates complete design systems for any product type with industry-specific reasoning rules.
theme-factory
Toolkit for styling artifacts with a theme. These artifacts can be slides, docs, reportings, HTML landing pages, etc. There are 10 pre-set themes with colors/fonts that you can apply to any artifact that has been creating, or can generate a new theme on-the-fly.
slack-gif-creator
Knowledge and utilities for creating animated GIFs optimized for Slack. Provides constraints, validation tools, and animation concepts. Use when users request animated GIFs for Slack like "make me a GIF of X doing Y for Slack."
skill-creator
Create new skills, modify and improve existing skills, and measure skill performance. Use when users want to create a skill from scratch, update or optimize an existing skill, run evals to test a skill, benchmark skill performance with variance analysis, or optimize a skill's description for better triggering accuracy.
shannon
Autonomous AI security pen testing. Executes real exploits against web applications to find SQL injection, XSS, SSRF, authentication flaws, and IDOR vulnerabilities. Reports only confirmed, reproducible findings — no false positives.
remotion
Create programmatic videos using React and Remotion. Translate natural language descriptions into working Remotion components for product demos, release announcements, explainer videos, and animated content.
planetscale
Design schemas and write queries for PlanetScale serverless MySQL using branch-based workflows. Ensures index coverage, avoids foreign key anti-patterns, and treats every schema change as a reviewable, reversible deploy request.
mcp-builder
Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).
karpathy-coding-principles
Apply Karpathy-inspired coding behavior guidelines — think before coding, prefer simplicity, make surgical changes, and execute toward verifiable goals. Reduces wrong assumptions, overengineering, and unintended side-effects.