| name | create-vip-workflow-tool |
| description | Scaffold a VIP Workflow editorial tool plugin. Use when the user wants to create a custom content analysis tool, validator, or checker for VIP Workflow. |
Create a VIP Workflow Editorial Tool
Editorial tools run against post content during workflow transitions or on demand from the editor sidebar. Each tool is a standalone WordPress plugin that registers an ability with the Abilities API.
Requirements
Before starting, gather from the user:
- What does the tool check? -- e.g., readability, brand compliance, SEO, fact-checking
- Plugin slug -- e.g.,
workflow-tool-readability
- Ability ID -- namespaced, e.g.,
workflow-tool-readability/readability-checker
- Does it call an external API? -- determines settings/availability patterns
- Should it run on workflow transitions? -- determines
transition_eligible meta
Plugin Structure
workflow-tool-{name}/
workflow-tool-{name}.php # Main plugin file
includes/
class-{name}-checker.php # Tool logic (register + execute)
The plugin directory lives alongside vip-workflow/ in the same parent directory.
Boilerplate
Main plugin file: workflow-tool-{name}/workflow-tool-{name}.php
<?php
declare( strict_types=1 );
namespace WorkflowTool{PascalName};
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
require_once __DIR__ . '/includes/class-{name}-checker.php';
add_action( 'wp_abilities_api_init', array( {PascalName}Checker::class, 'register' ) );
Checker class: workflow-tool-{name}/includes/class-{name}-checker.php
<?php
declare( strict_types=1 );
namespace WorkflowTool{PascalName};
class {PascalName}Checker {
public static function register(): void {
if ( ! function_exists( 'vip_workflow_register_ability' ) ) {
return;
}
vip_workflow_register_ability(
'workflow-tool-{name}/{slug}',
array(
'label' => __( '{Display Name}', 'workflow-tool-{name}' ),
'description' => __( '{One-line description.}', 'workflow-tool-{name}' ),
'category' => 'vip-workflow',
'input_schema' => self::get_input_schema(),
'output_schema' => self::get_output_schema(),
'execute_callback' => array( self::class, 'execute' ),
'permission_callback' => array( self::class, ),
=> (
=> ,
=> ,
=> ,
=> ( ),
=> ,
=> ,
=> (
=> (
=> ,
=> ,
=> ,
=> ,
=> ,
=> ,
=> ,
),
),
=> (
=> ,
=> ,
=> ,
),
),
)
);
}
{
( );
}
{
(
=> ,
=> (
=> ( => ),
),
=> ( ),
);
}
{
(
=> ,
=> (
=> ( => ),
=> ( => ),
=> ( => ),
),
);
}
{
= [] ?? ;
= ( );
( ! ) {
(
=> ,
=> ,
=> (),
);
}
= ::()
->( );
= [] ?? ;
= ( ->post_content );
= ::( );
= ( ) ? : ( , - ( ( ) * ) );
(
=> ,
=> ( )
?
: ( , ( ) ),
=> ,
);
}
{
= ();
;
}
}
Availability
A tool that needs configuration -- an API key, an external service, a plugin that may not be installed -- declares meta.availability_callback. AbilityExecutor gates execution on it, and the Tools page shows the result.
Write it against the structured shape from the start. Returning a bare false still works and is still silent, but it discards the reason: the surface can then only say that something is unconfigured, naming nothing and linking nowhere.
use VIPWorkflow\Abilities\Availability;
use VIPWorkflow\Abilities\RequirementFactory;
use VIPWorkflow\Abilities\RequirementGroup;
public static function check_availability(): bool|Availability {
$settings = \VIPWorkflow\Abilities\AbilitySettings::get_instance();
$options = $settings->get_options( 'workflow-tool-{name}/{slug}' );
if ( ! empty( $options['api_key'] ) ) {
return true;
}
return Availability::unmet(
RequirementGroup::all(
RequirementFactory::in_card(
'settings:workflow-tool-{name}',
__( '{Display Name} has no API key. Add it in the tool settings below.', 'workflow-tool-{name}' ),
__( , ),
( , ),
( ( , ) )
)
)
);
}
Rules:
- The callback owns satisfaction. Return
true as soon as the dependencies are met; an Availability::unmet() carries only unmet requirements, and nothing downstream re-evaluates them.
- A tool that generates text with AI does not write its own check. Generate through
VIPWorkflow\AI\AiInference::get_instance()->model(), never by naming a provider class, and gate with VIPWorkflow\Abilities\AiAvailability::for_selected_provider( $sources ). That pair is the whole contract: the check resolves exactly the three conditions the resolver needs — the selected provider registered with the WordPress AI Client, its credential, and a chosen model — so the gate and the generation call cannot disagree. Naming a provider yourself is how a Claude-configured site ends up being told to go get an OpenAI key. for_provider( $provider, $sources ) exists for the rare tool that genuinely requires one specific vendor. Do not call AiClient::isConfigured() in an availability callback — it makes a live request, and availability is read on every Tools-page load.
AiInference::model() returns null when the selection is unresolvable. There is no fallback to another vendor. Bail with a WP_Error rather than passing null to usingModel().
- Build requirements with
RequirementFactory, not by hand. Use in_card() when the value is entered in the tool's own settings_schema fields; missing_credential( $service, $service_label, $sources ) when the key is one the plugin reads through VIPWorkflow\AI\Credentials, which resolves the destination against the install instead of hardcoding a screen; dependency() when a prerequisite other than a credential is missing (e.g. a required plugin is not installed); unsupported_environment() when nothing can be configured to fix it.
- Supply both message registers. The admin reason may name a screen; the user message must not, because tools execute under
edit_posts while admin settings require manage_options. The register is chosen where the requirement is read, not where it is authored.
- Group with
RequirementGroup::all(), or RequirementGroup::any() when satisfying one member is enough -- an any group renders as one "configure at least one of" block.
- Returning remains valid for a tool with nothing useful to say. It produces the generic line, with no diagnostic.
Registration Rules
- The ability name must be namespaced with a slash:
plugin-slug/ability-slug. Never use sanitize_key() on ability IDs (it strips slashes).
category must be 'vip-workflow'
meta.type is required for the tool to appear in Integrations > Tools. Use 'check' for validation tools, 'helper' for content generators, 'validator' for analysis tools, 'agent' for AI Agent.
meta.transition_eligible = true if the tool should run during workflow or phase transitions
meta.show_in_commands = true to expose the "Show in Command Palette (⌘K)" toggle in Integrations > Tools; admins control the actual value per-site. Omit or set false for tools that should never appear in the palette.
- Hook into
wp_abilities_api_init
- Guard with
function_exists( 'vip_workflow_register_ability' )
- For phase transition tools (ideation to pitch/editorial): the input receives
project_id (not post_id). Do not use current_user_can( 'edit_post', ... ) in permission_callback because ideation CPTs use custom capabilities. Validate input and return true.
Output Contract
Declare what your tool returns. Set meta.result_type to one of three values.
The result modal switches on that declaration rather than inspecting your output
keys, so a shape it has never seen still renders correctly.
Do not skip this. Before result_type existed the modal guessed by probing keys
in priority order, and every new shape either matched no key and rendered an empty
modal or matched the wrong key and applied the wrong value to a field. A tool that
declares nothing still falls back to that guessing.
result_type => 'report' — a verdict with findings
For checks. Also the shape a transition gate reads.
status (string) -- 'pass', 'warning' or 'fail'. Always set it. Omitting
it means "no verdict", which is not the same as failing.
score (number, 0-100) -- optional
summary (string) -- one line about the result
issues (array) -- each issue has rule, message, severity ('error', 'warning', 'info')
result_type => 'value' — one value that replaces a field
For generators: an excerpt, a rewritten paragraph.
excerpt or content (string) -- the value itself
summary (string) -- optional
Pair it with meta.apply_field naming the field the value belongs in, or the
writer gets no way to use it.
result_type => 'list' — several options to choose from
For suggestions: alternative headlines, related links.
suggestions (array) -- one of two row shapes:
- a plain string, when the row is a value a field can be set to (an
alternative headline)
array( 'label' => …, 'meta' => …, 'href' => … ), when the row points
somewhere instead (a suggested link, whose label is anchor text)
summary (string) -- one line above the list
Only plain-string rows get an apply action, because only they are a value. Set
apply_field for those.
summary is never a value
It is the line about a result. It is displayed, never applied. A tool that
returned '5 suggested headlines.' as its summary once had that written into a
post title, because the modal treated the summary as content to apply.
Testing
- Place the plugin directory alongside
vip-workflow/
- Activate it in WordPress admin
- Go to Integrations > Tools to verify it appears
- Open a post in the editor; the tool should appear in the sidebar