WP Report Studio — developer guide
How to extend WP Report Studio from your own plugin, theme or mu-plugin without modifying it: add data sources and columns, react to report activity, and tune behaviour. Everything here is an ordinary WordPress hook or a small PHP contract.
A complete, working example plugin is reproduced in Example extension. It is also available as an installable zip (wrs-example-extension.zip) from the Developers page of the WP Report Studio website.
Compatibility policy
| Level | Meaning |
|---|---|
| Stable | Supported public API. Not removed or changed incompatibly in a minor release. Deprecations are announced in the changelog and kept working (with a _doing_it_wrong / do_action_deprecated notice) for at least two minor releases. |
| Beta | Supported and tested, but the shape may still change before 1.0 in response to developer feedback. New data is added to the end of argument lists or inside $context arrays, never reordered. |
| Tuning | Performance escape hatches. Supported, but the default may change and the hook may be retired when the underlying query strategy changes. |
| Internal | Not an API. Listed only so you know not to depend on it. |
Anything not listed in this guide (classes, methods, option names, table layouts, AJAX actions, JavaScript globals) is internal and can change in any release.
Every hook name starts with vrb_. All PHP API is available after plugins_loaded.
Lifecycle actions (Beta)
Fired after the outcome is final, so a listener can record or react but can’t veto. Use filters to change behaviour. Several of these run from WP-Cron or WP-CLI, so don’t assume a browser request or a logged-in user. If a listener throws, WP Report Studio catches and logs it (Report Studio → Diagnostics), the download or job carries on, and listeners after it on that action don’t run. Keep listeners fast; push slow work (HTTP calls) to a background job.
| Action | Arguments | When |
|---|---|---|
vrb_report_saved |
int $report_id, array $definition, bool $is_new |
A report is created (builder, duplicate, template, AI suggestion) or its definition/name saved. The builder autosaves, so this fires on most edits; defer heavy work. |
vrb_report_ran |
int $report_id, array $definition, array $context |
A saved report returned rows in the report viewer: each load, page, filter or sort change. $context: total, rows, page, per_page, duration_ms, user_id. $definition includes the viewer’s temporary filter/sort changes. Not fired for the builder’s live preview. |
vrb_report_run_failed |
int $report_id, WP_Error $error, array $context |
A viewer run hit a database error (run_sql_error) or exception (run_exception). Not fired for permission errors, bad input or the Free-plan Pro gate. $context: user_id. |
vrb_export_completed |
int $report_id, string $format, int $rows, array $context |
A CSV or XLSX export finished. $format: csv/xlsx. $rows excludes the header. $context['destination']: download or file (scheduled email attachment; $context['path'] is the temp file, deleted after sending). |
vrb_export_failed |
int $report_id, string $format, WP_Error $error, array $context |
An export could not be produced. Codes: not_configured, too_many_rows, open_failed, no_openspout. |
vrb_schedule_sent |
int $report_id, array $schedule, array $recipients, string $format |
A scheduled report was emailed (Pro). |
vrb_schedule_failed |
int $report_id, array $schedule, WP_Error $error |
A scheduled delivery failed (Pro). Codes: license_required, not_found, mail_failed, plus the export codes above. $schedule is [] if the report no longer exists. |
vrb_bulk_job_completed |
int $log_id, array $job |
A bulk action finished, synchronously or as a background job (Pro). $job: action_type, action_label, executed_by, target_count, total_targets, status. $log_id is the Undo handle. |
vrb_bulk_job_failed |
int $log_id, array $job |
A background bulk job stopped early: status is failed or cancelled (Pro). Fired once per transition. |
vrb_bulk_job_undone |
int $log_id, int $user_id |
A bulk action was undone (Pro). |
add_action( 'vrb_schedule_failed', function ( $report_id, $schedule, $error ) {
wp_mail( get_option( 'admin_email' ), 'Scheduled report failed',
get_the_title( $report_id ) . ': ' . $error->get_error_message() );
}, 10, 3 );
Filters (Stable)
Data and discovery
| Filter | Value → return | Arguments | Use it to |
|---|---|---|---|
vrb_reportable_post_types |
array of name => { name, label, description, count } |
— | Add or remove post types offered as a report’s root. Plugin-configuration types (ACF field groups, form definitions, …) and WooCommerce HPOS order placeholders are removed by default. |
vrb_query_post_statuses |
string[] (default publish, draft, private) |
— | Change which post statuses reports include. trash is always excluded. |
vrb_field_sources |
VRB_Field_Source[] |
— | Register a custom-fields plugin integration. See Field sources. |
vrb_virtual_providers |
string[] class names |
— | Add computed or external-table columns. See Virtual columns. |
vrb_extra_field_nodes |
array of field nodes (default []) |
string $post_type, array $claimed_keys |
Add plain postmeta-backed fields to the field tree (e.g. keys the sampler misses). Each node needs type and key; see Field nodes. |
vrb_known_analytics_meta |
array meta_key => [ label, data_type, source_name ] |
— | Label view/like counters stored in postmeta so they appear as named fields. |
vrb_repeater_max_size |
int (default 50) |
string $repeater_key |
Raise how many rows of an ACF repeater are expanded. |
vrb_default_sort_newest_first |
bool (default true) |
— | false to order unsorted reports oldest-first. |
Limits and caching
| Filter | Value → return | Arguments | Use it to |
|---|---|---|---|
vrb_query_max_rows |
int (default 50,000) |
— | Safety cap for exports and scheduled deliveries; past it they fail with too_many_rows instead of exhausting memory. 0 disables the cap. |
vrb_query_cache_ttl |
int seconds (default 30) |
— | How long identical report queries are reused (view → export). 0 disables. |
vrb_run_all_rows_threshold |
int (default 500) |
int $report_id, array $definition |
Up to this many rows, the viewer loads the whole result once and pages in the browser. |
Exports
| Filter | Value → return | Arguments | Use it to |
|---|---|---|---|
vrb_export_source_column_label |
string (default “Source”) |
— | Rename the optional edit-link column in CSV/XLSX. |
vrb_csv_write_utf8_bom |
bool (default true) |
array $definition |
Drop the UTF-8 byte-order mark for tools that dislike it (Excel needs it for non-ASCII text). |
vrb_csv_formula_guard |
bool (default true) |
— | Disable the spreadsheet formula-injection guard: non-numeric text starting with =, +, - or @, or with a tab or carriage return, gets a leading '. Only for trusted data. Applies to CSV; Excel downloads always store such text as text. |
vrb_csv_excel_long_id_guard |
bool (default true) |
— | Disable the guard that wraps long all-digit text values (IDs, phone numbers) as ="…" so Excel doesn’t turn them into scientific notation. |
vrb_excel_wrap_chars |
int (default 60) |
— | Characters per wrapped line assumed when estimating how tall an Excel cell with several details per line gets. Used only for the pre-download size warning. |
vrb_excel_row_lines |
int (default 27) |
— | Lines an Excel row can show (rows max out at 409 points). Cells estimated taller trigger the pre-download warning; the file itself is never cut. |
Permissions and bulk actions
| Filter | Value → return | Arguments | Use it to |
|---|---|---|---|
vrb_grant_caps_via_edit_posts |
bool (default true) |
— | false to stop edit_posts implying all Report Studio capabilities, so access comes only from the vrb_* capabilities. See Capabilities. |
vrb_bulk_required_post_caps |
string[] meta capabilities |
string $action_slug, array $params |
Per-post capabilities a user needs for a bulk action to touch a post (default edit_post; delete_post for trash; plus publish_post when publishing). Posts that fail are skipped and reported. |
vrb_bulk_sync_threshold |
int (default 100) |
string $action_slug, int[] $ids, array $params |
Above this many posts, a bulk action runs as a background job. |
vrb_bulk_chunk_size |
int (default 25) |
string $action_slug |
Posts per background-job chunk. |
Tuning
| Filter | Value → return | Arguments | Use it to |
|---|---|---|---|
vrb_query_select_hint |
string (STRAIGHT_JOIN on MySQL, '' on SQLite) |
array $definition |
Remove or replace the join-order hint if your MySQL version plans badly with it. |
vrb_query_use_two_phase |
bool |
array $definition |
Force or prevent the two-phase (IDs, then hydrate) fetch for complex reports. |
vrb_consolidate_meta_joins |
bool (default true) |
string $scope_key, string $post_id_expr |
Turn off the single-join postmeta optimisation for a scope while diagnosing a query. |
vrb_install_postmeta_composite_index |
bool (default true) |
— | false to stop the plugin adding its (post_id, meta_key) index to wp_postmeta (some managed hosts disallow ALTER TABLE). |
Internal (not supported)
| Hook | Why it isn’t an API |
|---|---|
vrb_upgrade_url |
Commercial upgrade links; may change with licensing. |
vrb_trial_url |
Commercial trial links. |
vrb_founders_coupons |
Launch pricing. |
vrb_checkout_coupon |
Launch pricing. |
vrb_show_kpi_cards |
Unfinished feature toggle. |
Extension contracts
Virtual columns
A virtual provider adds fields whose values come from SQL you supply. They appear in the field tree, and sort, filter, total and export like any other field. Providers are classes with four static methods, registered by class name through vrb_virtual_providers:
| Method | Returns |
|---|---|
is_available() |
bool. Return false to hide the fields (e.g. your table isn’t installed). |
fields( string $post_type ) |
Array of field nodes with type => 'virtual', provider => YourClass::class, a key, label and data_type. |
sql_join( string $field_key ) |
LEFT JOIN … clause(s), or '' when the expression needs no join. Use a table alias unique to your plugin. |
sql_expr( string $field_key ) |
SQL expression for the value. The report’s root post is p_root; rows are grouped by p_root.ID, so aggregate joined rows (SUM(...), MAX(...)) or use a correlated subquery. |
This SQL is compiled into the report query as trusted code. Build it only from constants and $wpdb->prefix; never interpolate request data or field values. Filters a user applies to the column are escaped by WP Report Studio.
Field sources
To integrate a custom-fields plugin (the way ACF, Meta Box and Pods are supported), implement VRB_Field_Source and add an instance through vrb_field_sources:
interface VRB_Field_Source {
public function id(); // 'my-fields'
public function is_active(); // true only when your plugin is loaded
public function discover( $post_type, array &$tree, array &$claimed_keys, array &$repeater_roots );
}
discover() appends field nodes for $post_type to $tree, marks the meta keys it owns in $claimed_keys (key => true) so the raw postmeta scan doesn’t list them twice, and lists repeater root keys in $repeater_roots. Postmeta-stored fields need no query code. Data in a custom table or join table is described with a storage descriptor; follow includes/class-source-pods.php, the reference implementation for both. Field sources are Stable as an interface; the storage descriptor shape is Beta.
Field nodes
A field node is an array:
| Key | Required | Notes |
|---|---|---|
type |
yes | meta (postmeta), virtual, relationship, repeater. |
key |
yes | Meta key, or your virtual field key. |
label |
recommended | Shown in the builder. |
data_type |
recommended | text, number, date, boolean, email, url, attachment, user, taxonomy_ref. Drives filters, totals and formatting. |
choices |
optional | value => label for select-style fields; shown as labels and offered as filter values. |
provider |
virtual only | Your provider class name. |
Capabilities
Access is controlled by five capabilities (constants on VRB_Capabilities): vrb_view_reports (VIEW), vrb_edit_reports (EDIT), vrb_export_reports (EXPORT), vrb_run_bulk_actions (BULK), vrb_manage_schedules (SCHEDULE). Grant them to custom roles with add_cap(). By default edit_posts implies all five; turn that off with vrb_grant_caps_via_edit_posts.
Custom bulk actions
Not yet a supported API. The built-in actions use an internal registry whose snapshot/undo contract is still changing; if you need one, tell us what it should do.
Example extension
The example adds an “Approved comments” column to every post type, records lifecycle activity (inspect it with wp wrs-example activity), and includes scheduled posts in reports.
<?php
/**
* Plugin Name: WP Report Studio — Example Extension
* Requires Plugins: wp-report-studio
*/
class WRS_Example_Comment_Count_Provider {
public static function is_available() {
return true;
}
public static function fields( $post_type ) {
return array(
array(
'type' => 'virtual',
'provider' => __CLASS__,
'key' => 'approved_comments',
'label' => 'Approved comments',
'data_type' => 'number',
),
);
}
public static function sql_join( $field_key ) {
return '';
}
public static function sql_expr( $field_key ) {
global $wpdb;
return "(SELECT COUNT(*) FROM {$wpdb->comments} wrs_c WHERE wrs_c.comment_post_ID = p_root.ID AND wrs_c.comment_approved = '1')";
}
}
add_filter( 'vrb_virtual_providers', function ( $providers ) {
$providers[] = 'WRS_Example_Comment_Count_Provider';
return $providers;
} );
add_action( 'vrb_export_completed', function ( $report_id, $format, $rows, $context ) {
error_log( sprintf( 'Report %d exported: %d rows as %s (%s)', $report_id, $rows, $format, $context['destination'] ) );
}, 10, 4 );
add_filter( 'vrb_query_post_statuses', function ( $statuses ) {
$statuses[] = 'future';
return array_unique( $statuses );
} );
Testing your integration
- Run your listener against real activity: open a report, export it, and check the action fired (the example’s
wp wrs-example activityshows how). - Assert on hook arguments in your own tests with
did_action()or a recording listener; argument shapes are documented above. - A WordPress Playground blueprint is the quickest disposable test site. WP Report Studio’s own SQLite parity harness (
tests/playground/in the source) shows the pattern.
Reporting gaps
If you need a hook that isn’t here, or an argument you can’t get, open a support request describing the integration. Lifecycle actions are Beta precisely so they can change in response.