<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: WPVibes</title>
    <description>The latest articles on DEV Community by WPVibes (@wpvibes).</description>
    <link>https://dev.to/wpvibes</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3978746%2F4a5ecd1b-f11b-4194-b1fb-1a490fef4667.jpg</url>
      <title>DEV Community: WPVibes</title>
      <link>https://dev.to/wpvibes</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wpvibes"/>
    <language>en</language>
    <item>
      <title>How to Connect Elementor Form Submissions to Google Sheets</title>
      <dc:creator>WPVibes</dc:creator>
      <pubDate>Mon, 03 Aug 2026 10:23:40 +0000</pubDate>
      <link>https://dev.to/wpvibes/how-to-connect-elementor-form-submissions-to-google-sheets-4084</link>
      <guid>https://dev.to/wpvibes/how-to-connect-elementor-form-submissions-to-google-sheets-4084</guid>
      <description>&lt;p&gt;If you want to send your Elementor form to Google Sheets automatically, Elementor Pro doesn't offer a built-in way to do it. Most guides send you to Zapier, or a single-purpose connector plugin that only does one thing.&lt;/p&gt;

&lt;p&gt;Form Vibes Pro connects Elementor forms to Google Sheets natively, from inside WordPress. No automation platform. No per-task limits. Every new Elementor submission lands in your spreadsheet automatically.&lt;/p&gt;

&lt;p&gt;This guide walks through the complete setup, from Google API credentials to your first synced entry.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Does This Setup Give You?
&lt;/h2&gt;

&lt;p&gt;Once this is set up, every Elementor submission appears in Google Sheets within seconds, with no manual export needed. Your team or client can see leads live without needing WordPress access. Every entry is also saved to your WordPress database as a backup, so nothing depends on Google alone.&lt;/p&gt;

&lt;p&gt;Elementor Pro doesn't give you a way to send form data anywhere but email. Form Vibes Pro adds the missing Google Sheets connection, plus a full submission dashboard inside WordPress.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is Google Sheets Sync a Free or Pro Feature?
&lt;/h2&gt;

&lt;p&gt;Google Sheets sync is a Form Vibes Pro feature. Here's what's relevant to this guide:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Free&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Pro&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Save Elementor entries to WordPress database&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Export to CSV on demand&lt;/td&gt;
&lt;td&gt;Yes (up to 1,000 rows)&lt;/td&gt;
&lt;td&gt;Yes (unlimited)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automatic sync to Google Sheets&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filter which submissions get synced&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://formvibes.com/general/form-vibes-free-vs-pro/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog_content&amp;amp;utm_content=form_vibes"&gt;&lt;strong&gt;See full Pro feature list.&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Elementor Submissions vs Google Sheets Sync: Which Should You Use?
&lt;/h2&gt;

&lt;p&gt;Elementor Pro has a native Submissions screen inside WordPress. Use it for quick in-dashboard checks. Use Google Sheets sync when submissions need to reach people or tools outside WordPress in real time, such as sales teams, clients, external dashboards, or backup workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Connect Elementor Form Submissions to Google Sheets
&lt;/h2&gt;

&lt;p&gt;Setup has three parts: create Google API credentials, connect Form Vibes to your Google account, then set up the Google Sheets connection for your Elementor form.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create Your Google API Credentials
&lt;/h3&gt;

&lt;p&gt;Google requires API credentials so Form Vibes can communicate securely with your Google account. These consist of a Client ID and a Client Secret, generated in Google Cloud Console. The full credential creation process is covered in a dedicated guide. Follow that guide to &lt;a href="https://docs.formvibes.com/article/152-how-to-connect-with-google-sheet" rel="noopener noreferrer"&gt;generate your Client ID and Client Secret&lt;/a&gt;, then return here to continue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connect Form Vibes to Google Sheets
&lt;/h3&gt;

&lt;p&gt;Go to Form Vibes &amp;gt; Settings &amp;gt; Google Sheet in your WordPress admin. Paste your Client ID and Client Secret into their respective fields. Click Authenticate and sign in with your Google account when prompted. After approving the requested permissions, the connection status updates to show the account as connected.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpjdgqh12lnzcya8c2gxg.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpjdgqh12lnzcya8c2gxg.webp" alt="Connect Form Vibes to Google Sheets" width="800" height="539"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Tip:&lt;/strong&gt; During authentication, Google shows an "App is not verified" warning. This is expected for a self-configured OAuth connection. Click Show Advanced, then Go to App, and approve all requested permissions. The connection completes normally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set Up the Google Sheets Connection for Your Elementor Form
&lt;/h3&gt;

&lt;p&gt;With your Google account connected, go to Form Vibes &amp;gt; Export Profiles and click Add New. Under Type, select Google Sheet instead of Local Download. This tells Form Vibes to push submissions to a spreadsheet rather than generate a downloadable file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgwu4057582kkm14ujnwi.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgwu4057582kkm14ujnwi.webp" alt="Choose Google Sheet Profile Type in Export Profile" width="800" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configure the Export Profile Settings:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;From the form dropdown, select the Elementor form whose submissions you want to send to Google Sheets. Forms appear here once they've received at least one submission (see the troubleshooting section below if your form isn't listed yet).&lt;/p&gt;

&lt;p&gt;Choose the Google Spreadsheet where data should be sent. If the spreadsheet you need does not appear, check that the connected Google account has access to it. To create a new spreadsheet directly from Form Vibes, click the plus icon next to the spreadsheet selector.&lt;/p&gt;

&lt;p&gt;Select the specific worksheet, or tab, within the spreadsheet. Create a new one using the plus icon if needed.&lt;/p&gt;

&lt;p&gt;The Data Filter section lets you sync only the submissions that match specific conditions, instead of sending every submission to Google Sheets. To filter, choose whether any condition must be met (OR) or all conditions must be met (AND). Then, for each condition, select the form field to check, choose a match type of Equal, Not Equal, Contain, or Not Contain, and enter the value to match against. Click Add New Condition to stack additional rules.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuiaw99d3wu13j3snqx3o.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuiaw99d3wu13j3snqx3o.webp" alt="Choose Elementor Form and Filter Data" width="799" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Map Your Fields and Publish:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Map each Elementor form field to its corresponding spreadsheet column. If your Elementor form has a Name field and your sheet has a Name column, connect them here. Repeat for every field you want to appear in the spreadsheet. Click Publish to save and activate the connection.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm5ya4z6r0wfww5denyra.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm5ya4z6r0wfww5denyra.webp" alt="Field-to-column mapping for an Elementor form" width="707" height="536"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Important:&lt;/strong&gt; The Export Profile must be in Published status for submissions to sync automatically. One left in Draft will not push any data to the spreadsheet. Always confirm it is published before sending a test submission.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens After Someone Submits Your Elementor Form?
&lt;/h2&gt;

&lt;p&gt;Once the connection is published, no further action is needed. When a visitor submits your Elementor form, Form Vibes captures the submission, saves it to your WordPress database, and immediately pushes a new row to the connected Google Spreadsheet. Each Elementor form field appears in the column it was mapped to during setup.&lt;/p&gt;

&lt;p&gt;The submission appears in the sheet within moments. Anyone with access to the spreadsheet can see the new row without logging into WordPress. The spreadsheet keeps updating automatically with every new Elementor submission from that point forward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Aren't Elementor Submissions Reaching Google Sheets?
&lt;/h2&gt;

&lt;p&gt;Elementor submissions usually fail to reach Google Sheets because of one of six issues: a form not yet listed in the dropdown, a spreadsheet the connected account can't access, a redirect URI mismatch, a disabled Google API, an unpublished sync setup, or a column mapping that's fallen out of date. Submit a test entry from the frontend of your Elementor form first, then check the causes below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Elementor form is not showing in the Form dropdown.&lt;/strong&gt; Confirm Elementor Pro is active. Then confirm the form has received at least one submission on the frontend since Form Vibes was activated. Form Vibes only lists forms that have already received at least one submission, so a form with zero submissions will not appear in the dropdown yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spreadsheet is not showing in the dropdown.&lt;/strong&gt; Check that the Google account connected in Form Vibes Settings has access to the spreadsheet. If the spreadsheet is owned by a different Google account, share it with the connected account before trying again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redirect URI mismatch error.&lt;/strong&gt; The redirect URI entered in your Google Cloud Console app settings must match exactly the one Form Vibes provides. Open your Google Cloud Console project, navigate to OAuth credentials, and verify the redirect URI matches the one shown in Form Vibes Settings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;App verification warning during authentication.&lt;/strong&gt; This is expected for self-configured OAuth apps. Click Show Advanced, then Go to App, and approve all permissions. The warning does not indicate a problem with the connection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data is appearing in the wrong columns.&lt;/strong&gt; Open the Export Profile and review the column mapping section. Update the field-to-column pairings if your sheet headers or Elementor form field names have changed since it was last saved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Sheets and Drive APIs are not enabled.&lt;/strong&gt; Both APIs must be enabled inside your Google Cloud Console project. If sync silently fails, this is the second most common cause after a redirect URI mismatch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Form Vibes Pro connects Elementor forms to Google Sheets natively, without a third-party automation platform. Set up the Google API credentials once, connect your Google account, configure the sync for your form, and every new submission lands in your spreadsheet automatically.&lt;/p&gt;

&lt;p&gt;Every submission is also saved to your own WordPress database. So if you use other form plugins alongside Elementor, all your submissions live in one dashboard with the same Google Sheets sync, CSV export, filtering, and analytics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does Elementor Pro have a built-in Google Sheets integration?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Elementor Pro Forms include native actions for database, email, webhooks, and a handful of marketing tools, but Google Sheets is not one of them. To sync Elementor submissions to Google Sheets you need a plugin like Form Vibes Pro.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need Form Vibes Pro to connect Elementor forms to Google Sheets?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. The Google Sheets integration is a Form Vibes Pro feature. The free version still saves every Elementor submission to your WordPress database and lets you export to CSV on demand. The automatic real-time push to a Google Spreadsheet is the Pro upgrade.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I sync multiple Elementor forms to different Google Sheets?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Set up one sync connection per Elementor form, and point each one to its own spreadsheet or its own worksheet inside the same spreadsheet. There is no limit on how many Elementor forms you can connect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the same integration work with my other form plugins?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. The Google Sheets sync in Form Vibes Pro works with Contact Form 7, WPForms, Gravity Forms, Ninja Forms, WS Form, Bricks Builder forms, Beaver Builder forms, and Everest Forms. One plugin, one Google connection, all your forms.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>elementorform</category>
      <category>googlesheet</category>
    </item>
    <item>
      <title>WordPress Abilities API + MCP: The Developer's Complete Guide</title>
      <dc:creator>WPVibes</dc:creator>
      <pubDate>Tue, 28 Jul 2026 05:34:11 +0000</pubDate>
      <link>https://dev.to/wpvibes/wordpress-abilities-api-mcp-the-developers-complete-guide-4kpb</link>
      <guid>https://dev.to/wpvibes/wordpress-abilities-api-mcp-the-developers-complete-guide-4kpb</guid>
      <description>&lt;p&gt;AI tools like Claude, Cursor, and ChatGPT can now interact directly with your WordPress site, but only if your site knows how to describe what it can do.&lt;/p&gt;

&lt;p&gt;That's exactly what the WordPress Abilities API solves.&lt;/p&gt;

&lt;p&gt;It lets developers expose WordPress functionality as reusable, self-describing abilities that can be used by PHP, JavaScript, the REST API, and AI tools through the Model Context Protocol (MCP).&lt;/p&gt;

&lt;p&gt;Here's what this guide walks you through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Understand what the Abilities API is, in simple words&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create your first custom ability, step by step:&amp;nbsp;&lt;strong&gt;Get Posts by Post Type&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Test that ability two different ways&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Connect it to Claude Desktop using the official MCP Adapter&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Build two more real abilities: one that adds missing descriptions to tags and categories, and one that gives you a full site health report.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every piece of code in this guide is real and tested. Let's start from zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is the WordPress Abilities API?
&lt;/h2&gt;

&lt;p&gt;The WordPress Abilities API is a feature introduced in WordPress 6.9 that gives plugins, themes, and WordPress core a standard way to describe what they can do.&lt;/p&gt;

&lt;p&gt;Before understanding the API, let's first understand what an&amp;nbsp;&lt;strong&gt;ability&lt;/strong&gt;&amp;nbsp;is.&lt;/p&gt;

&lt;p&gt;Think of an ability as a single action that your WordPress site can perform. For example, getting a list of posts, publishing a page, generating a maintenance report, or updating a WooCommerce product. These actions already exist in WordPress, but until now, there wasn't a standard way to describe them so that other plugins, applications, or AI tools could easily discover and use them.&lt;/p&gt;

&lt;p&gt;The Abilities API solves this problem by letting developers register each action as an&amp;nbsp;&lt;strong&gt;ability&lt;/strong&gt;. Along with the code that performs the action, you also describe what the ability does, what information it needs, what it returns, and who is allowed to execute it.&lt;/p&gt;

&lt;p&gt;Every ability includes a few basic pieces of information:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;In our example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Name&lt;/td&gt;
&lt;td&gt;A unique identifier for the ability&lt;/td&gt;
&lt;td&gt;wpvibes/get-posts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Description&lt;/td&gt;
&lt;td&gt;Explains what the ability does&lt;/td&gt;
&lt;td&gt;Returns a list of posts filtered by post type, status, and limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input&lt;/td&gt;
&lt;td&gt;The information the ability needs&lt;/td&gt;
&lt;td&gt;post_type, status, and limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;The information the ability returns&lt;/td&gt;
&lt;td&gt;A list of posts with ID, title, status, date, and link&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permission&lt;/td&gt;
&lt;td&gt;Who is allowed to run the ability&lt;/td&gt;
&lt;td&gt;Users who can edit posts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For example, suppose you create an ability called&amp;nbsp;&lt;code&gt;wpvibes/get-posts&lt;/code&gt;. Instead of another plugin or AI tool needing to understand your plugin's internal code, it simply looks at the ability's definition to learn what inputs it accepts, what output it returns, and whether the current user has permission to run it.&lt;/p&gt;

&lt;p&gt;The actual PHP code that performs the work still lives inside your plugin. The Abilities API simply provides a standard way to describe that functionality so it can be discovered and used consistently across WordPress.&lt;/p&gt;

&lt;p&gt;Now that you understand what an ability is and what information it contains, let's build one from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before writing any code, make sure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WordPress 6.9 or later&lt;/strong&gt;&amp;nbsp;(the Abilities API has been in core since 6.9; 7.0+ is recommended)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A&amp;nbsp;&lt;strong&gt;local development site&lt;/strong&gt;&amp;nbsp;(LocalWP, WordPress Studio, or similar)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WP-CLI&lt;/strong&gt;&amp;nbsp;installed and working (needed for testing and for the Claude connection later)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Claude Desktop&lt;/strong&gt;&amp;nbsp;installed (the free plan is sufficient for this guide)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A code editor and basic PHP knowledge&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Notes:&lt;/strong&gt; Do everything in this guide on a local or staging site first, never directly on a live production site. The MCP Adapter is still a young project (v0.5.0 at the time of writing) and is evolving quickly. Always check the&amp;nbsp;&lt;a rel="noopener noreferrer nofollow" href="https://github.com/WordPress/mcp-adapter/releases"&gt;official documentation&lt;/a&gt;&amp;nbsp;if something looks different from what's shown here.&lt;/p&gt;

&lt;p&gt;With those ready, there's one more thing to set up before writing any actual ability code: a place for that code to live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a Plugin for Your Code
&lt;/h2&gt;

&lt;p&gt;All the code in this guide lives inside one small custom plugin. Here's how to set it up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Create a new folder and file at&amp;nbsp;&lt;code&gt;wp-content/plugins/wpvibes-abilities/wpvibes-abilities.php&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Paste in this basic plugin header:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="cd"&gt;/**
 * Plugin Name: WPVibes Abilities Demo
 * Description: Custom abilities for the Abilities API tutorial.
 * Version:     1.0.0
 * Author:      WPVibes
 */&lt;/span&gt;

&lt;span class="c1"&gt;// Exit if accessed directly.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'ABSPATH'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Activate the plugin from&amp;nbsp;&lt;strong&gt;wp-admin → Plugins&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It does nothing yet, we'll add abilities to it one at a time.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fapxofv68xzaj1itqdpdw.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fapxofv68xzaj1itqdpdw.webp" alt="Activate the Created Custom Plugin in WordPress" width="800" height="326"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Important: &lt;/strong&gt;Never put ability registration code directly in your theme's &lt;em&gt;functions.php&lt;/em&gt; on a real project. Abilities are functionality, not design, they belong in a plugin, so they keep working even if the theme changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create Your First Ability, "Get Posts by Post Type"
&lt;/h2&gt;

&lt;p&gt;Now that you understand what an ability is, it's time to create one. In this step, we'll build a simple&amp;nbsp;&lt;code&gt;wpvibes/get-posts&lt;/code&gt;&amp;nbsp;ability that returns a list of posts based on the post type, status, and limit you specify.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1.1: Register an ability category
&lt;/h3&gt;

&lt;p&gt;Every ability belongs to a category, this helps tools group and display abilities. Add this to&amp;nbsp;&lt;code&gt;wpvibes-abilities.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cd"&gt;/**
 * Step 1.1: Register a category for our abilities.
 */&lt;/span&gt;
&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wp_abilities_api_categories_init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes_register_ability_category'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpvibes_register_ability_category&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;wp_register_ability_category&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'wpvibes-tools'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'label'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'WPVibes Tools'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-abilities'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Demo abilities from the WPVibes tutorial.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-abilities'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Notes:&lt;/strong&gt; Categories are registered on their own hook &lt;code&gt;(wp_abilities_api_categories_init)&lt;/code&gt;, while abilities are registered on a different one &lt;code&gt;(wp_abilities_api_init)&lt;/code&gt;. Mixing these two up is one of the most common first-time mistakes. If your ability doesn't appear anywhere, check the hooks first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1.2: Register the ability
&lt;/h3&gt;

&lt;p&gt;With the category in place, you can now register your first ability. Register the&amp;nbsp;&lt;code&gt;wpvibes/get-posts&lt;/code&gt;&amp;nbsp;ability by adding the following code below your category registration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cd"&gt;/**
 * Step 1.2: Register the "Get Posts" ability.
 */&lt;/span&gt;
&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wp_abilities_api_init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes_register_get_posts_ability'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpvibes_register_get_posts_ability&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nf"&gt;wp_register_ability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'wpvibes/get-posts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'label'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Get Posts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-abilities'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Returns a list of posts filtered by post type, status, and limit.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-abilities'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'category'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-tools'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

            &lt;span class="c1"&gt;// What data this ability ACCEPTS.&lt;/span&gt;
            &lt;span class="s1"&gt;'input_schema'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'properties'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s1"&gt;'post_type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="s1"&gt;'type'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'The post type to fetch. Example: post, page, product.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'default'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'post'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="s1"&gt;'status'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="s1"&gt;'type'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Post status to filter by.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'enum'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'publish'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'draft'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'future'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="s1"&gt;'default'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'publish'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="s1"&gt;'limit'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="s1"&gt;'type'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'integer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'How many posts to return (1-20).'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'default'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'minimum'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'maximum'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="c1"&gt;// What data this ability RETURNS.&lt;/span&gt;
            &lt;span class="s1"&gt;'output_schema'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'array'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'items'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s1"&gt;'type'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'properties'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="s1"&gt;'id'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'integer'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="s1"&gt;'title'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="s1"&gt;'date'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="s1"&gt;'link'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="c1"&gt;// The actual logic.&lt;/span&gt;
            &lt;span class="s1"&gt;'execute_callback'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes_get_posts_callback'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

            &lt;span class="c1"&gt;// Who can run it.&lt;/span&gt;
            &lt;span class="s1"&gt;'permission_callback'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;current_user_can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'edit_posts'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;

            &lt;span class="c1"&gt;// Make it visible to AI agents through MCP (used later in this guide).&lt;/span&gt;
            &lt;span class="s1"&gt;'meta'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'mcp'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s1"&gt;'public'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Understanding the Registration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Although the registration looks long, most of it is simply describing the ability.&lt;/p&gt;

&lt;p&gt;The important parts are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;'wpvibes/get-posts': the unique name. Always use your own namespace (your plugin or company name) so it never conflicts with other plugins.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;input_schema: this is JSON Schema format. It tells any consumer, human, tool, or AI, "you can send me&amp;nbsp;post_type,&amp;nbsp;status, and&amp;nbsp;limit, and here are the rules for each." Notice the&amp;nbsp;enum&amp;nbsp;for status, the AI now knows exactly which values are valid, without guessing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;output_schema: describes what comes back. This lets AI tools understand your response without any extra explanation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;permission_callback: the security gate. Here we require the&amp;nbsp;edit_posts&amp;nbsp;capability, so only users who can edit posts may run this ability.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;meta.mcp.public =&amp;gt; true: this single line is what will later make the ability visible to Claude through the MCP Adapter.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Important: &lt;/strong&gt;Never use&amp;nbsp;&lt;code&gt;'permission_callback' =&amp;gt; '__return_true'&lt;/code&gt;&amp;nbsp;in production code. Every ability must have a real permission check. When AI agents can discover and call your abilities, the permission callback is your main line of defense.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1.3: Write the execute callback
&lt;/h3&gt;

&lt;p&gt;Registering an ability only describes&amp;nbsp;&lt;strong&gt;what&lt;/strong&gt;&amp;nbsp;it does.&lt;/p&gt;

&lt;p&gt;The actual work happens inside the&amp;nbsp;&lt;strong&gt;execute callback&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Whenever someone runs&amp;nbsp;&lt;code&gt;wpvibes/get-posts&lt;/code&gt;, WordPress calls this function automatically. It receives the validated input from the schema, performs the requested task, and returns the result.&lt;/p&gt;

&lt;p&gt;Add the following function below your ability registration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cd"&gt;/**
 * Step 1.3: The logic that runs when the ability is executed.
 *
 * @param array $input Validated input from the schema.
 * @return array|WP_Error
 */&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpvibes_get_posts_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// Sanitize inputs (yes, even though the schema already validates them).&lt;/span&gt;
    &lt;span class="nv"&gt;$post_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'post_type'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;sanitize_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'post_type'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'post'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$status&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;sanitize_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'publish'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$limit&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'limit'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;absint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'limit'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Check the post type actually exists on this site.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nf"&gt;post_type_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$post_type&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WP_Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'invalid_post_type'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'The post type "%s" does not exist on this site.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$post_type&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_posts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'post_type'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$post_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'post_status'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'posts_per_page'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Build the response to match our output schema.&lt;/span&gt;
    &lt;span class="nv"&gt;$results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$posts&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$results&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'id'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="no"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'title'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;get_the_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;post_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'date'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;get_the_date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Y-m-d'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'link'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;get_permalink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$results&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What This Callback Does&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sanitize inputs anyway.&lt;/strong&gt;&amp;nbsp;The schema validates types, but defense in depth is a WordPress best practice.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Validate important conditions.&lt;/strong&gt;&amp;nbsp;Before querying posts, we check that the requested post type actually exists.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Perform the requested task.&lt;/strong&gt;&amp;nbsp;We call&amp;nbsp;&lt;code&gt;get_posts()&lt;/code&gt;&amp;nbsp;using the values supplied by the user.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Return structured data.&lt;/strong&gt;&amp;nbsp;Instead of returning raw WordPress objects, we build a clean response that matches the&amp;nbsp;&lt;code&gt;output_schema&lt;/code&gt;&amp;nbsp;we defined earlier.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If something goes wrong, return an&amp;nbsp;&lt;code&gt;WP_Error&lt;/code&gt;&amp;nbsp;object instead of an empty response. This gives applications and AI tools a clear explanation of what failed.&lt;/p&gt;

&lt;p&gt;Congratulations! You've successfully created and registered your first WordPress ability. Before we connect it to Claude, it's a good idea to verify that everything works correctly. We'll test the ability in two ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Using PHP with WP-CLI&lt;/strong&gt;&amp;nbsp;to confirm the ability executes correctly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Using the official WordPress AI plugin&lt;/strong&gt;&amp;nbsp;to test it through a visual interface.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once both tests are successful, we'll connect the ability to Claude using the MCP Adapter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Test the Ability with the AI Plugin
&lt;/h2&gt;

&lt;p&gt;WP-CLI is fast and convenient for developers, but sometimes it's easier to test an ability visually.&lt;/p&gt;

&lt;p&gt;The official&amp;nbsp;&lt;a href="https://wordpress.org/plugins/ai/" rel="noopener noreferrer"&gt;&lt;strong&gt;AI plugin&lt;/strong&gt;&lt;/a&gt;&amp;nbsp;in WordPress includes an&amp;nbsp;&lt;strong&gt;Abilities Explorer&lt;/strong&gt;&amp;nbsp;that lets you discover registered abilities, provide input values, and view the response, all from within the WordPress admin area.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Go to&amp;nbsp;&lt;strong&gt;Plugins → Add New&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Search for&amp;nbsp;&lt;strong&gt;AI&lt;/strong&gt;, then install and activate the plugin.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open&amp;nbsp;&lt;strong&gt;Settings → AI&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Turn on&amp;nbsp;&lt;strong&gt;Enable AI&lt;/strong&gt;&amp;nbsp;at the top of the page.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open the&amp;nbsp;&lt;strong&gt;Admin Experiments&lt;/strong&gt;&amp;nbsp;tab and enable the&amp;nbsp;&lt;strong&gt;Abilities Explorer&lt;/strong&gt;&amp;nbsp;feature.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frfjvury6xvm9tt6kvi3x.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frfjvury6xvm9tt6kvi3x.webp" alt="Enable Abilities Explorer Option in AI Plugin WordPress" width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Test Your Ability
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Go to&amp;nbsp;&lt;strong&gt;Tools → Abilities Explorer&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Find your&amp;nbsp;&lt;code&gt;wpvibes/get-posts&lt;/code&gt;&amp;nbsp;ability and click&amp;nbsp;&lt;strong&gt;Test&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ypyxfq9zidjl4n59jfz.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ypyxfq9zidjl4n59jfz.webp" alt="Check All Abilities and Test Them Using AI Plugin" width="800" height="231"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Click&amp;nbsp;Invoke Ability.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The&amp;nbsp;Result&amp;nbsp;section displays the data returned by your ability.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If everything is configured correctly, you should see the same output you received when testing with WP-CLI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2upu8iij34tr2xvega3l.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2upu8iij34tr2xvega3l.webp" alt="Test Ability By Clicking on Invoke Ability" width="710" height="639"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Test the Ability with PHP (WP-CLI)
&lt;/h2&gt;

&lt;p&gt;Before connecting any AI, always test the ability directly. If something breaks later during MCP setup, this test tells you whether the problem is in your ability or in the connection.&lt;/p&gt;

&lt;p&gt;The fastest way is WP-CLI. Run this from your site's folder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;wp&lt;/span&gt; &lt;span class="k"&gt;eval&lt;/span&gt; &lt;span class="s1"&gt;'
$ability = wp_get_ability( "wpvibes/get-posts" );
if ( ! $ability ) { echo "Ability not found!"; exit; }
$result = $ability-&amp;gt;execute( array( "post_type" =&amp;gt; "post", "limit" =&amp;gt; 3 ) );
if ( is_wp_error( $result ) ) {
    echo "Error: " . $result-&amp;gt;get_error_message();
} else {
    print_r( $result );
}
'&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;admin&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, ' --user=[your-user-name].&lt;/p&gt;

&lt;p&gt;You should see an array of your latest 3 posts with their IDs, titles, and links&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frn7s79gn0j2x2jbp3a0t.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frn7s79gn0j2x2jbp3a0t.webp" alt="Test Ability Using WP - CLI Command" width="800" height="790"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can also execute the same ability anywhere in your own plugin or theme code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'admin_init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nv"&gt;$ability&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_get_ability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes/get-posts'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$ability&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$ability&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'post_type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'page'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'limit'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;pre&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nb"&gt;print_r&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;/pre&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Tips:&lt;/strong&gt; Always check the result with&amp;nbsp;&lt;code&gt;is_wp_error( $result )&lt;/code&gt;. A&amp;nbsp;&lt;code&gt;WP_Error&lt;/code&gt;&amp;nbsp;object is truthy in PHP, so a simple&amp;nbsp;&lt;code&gt;if ( $result )&lt;/code&gt;&amp;nbsp;check will pass even when the ability failed. This is one of the most common bugs when working with abilities.&lt;/p&gt;

&lt;p&gt;This "PHP first" test also shows the first real use case: any plugin can now call any other plugin's abilities through one standard interface, with permissions checked automatically. No hard-coded function names, no tight coupling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Connect Your Ability to MCP(Claude) with the MCP Adapter
&lt;/h2&gt;

&lt;p&gt;So far, you've created and tested your ability inside WordPress. Now it's time to make it available to an AI assistant.&lt;/p&gt;

&lt;p&gt;To do that, we'll use the&amp;nbsp;&lt;strong&gt;official WordPress MCP Adapter&lt;/strong&gt;. Before setting it up, let's quickly understand how everything fits together.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Is the MCP Adapter?
&lt;/h3&gt;

&lt;p&gt;The&amp;nbsp;&lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt;&amp;nbsp;is an open standard that lets AI applications communicate with external systems.&lt;/p&gt;

&lt;p&gt;The&amp;nbsp;&lt;strong&gt;WordPress MCP Adapter&lt;/strong&gt;&amp;nbsp;is an official plugin that acts as a bridge between your WordPress site and AI clients.&lt;/p&gt;

&lt;p&gt;Instead of talking directly to your plugin, Claude communicates with the MCP Adapter. The adapter discovers all abilities you've registered with the Abilities API and exposes them as tools that Claude can use.&lt;/p&gt;

&lt;p&gt;The overall flow looks like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude Desktop
   ↓ Sends the request
MCP Adapter
   ↓ Connects Claude to WordPress
Abilities API
   ↓ Finds and validates the ability
wpvibes/get-posts
   ↓ Runs the callback and returns posts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4.1: Install the MCP Adapter
&lt;/h3&gt;

&lt;p&gt;The adapter isn't in the plugin directory yet. Install it from GitHub:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Go to the&amp;nbsp;&lt;a href="https://github.com/WordPress/mcp-adapter/releases" rel="noopener noreferrer"&gt;MCP Adapter releases page&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Download the latest&amp;nbsp;&lt;a href="http://mcp-adapter.zip" rel="noopener noreferrer"&gt;&lt;code&gt;mcp-adapter.zip&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In wp-admin:&amp;nbsp;&lt;strong&gt;Plugins → Add New → Upload Plugin&lt;/strong&gt;&amp;nbsp;→ upload the zip → Activate&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Verify it's working with WP-CLI&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wp mcp-adapter list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see at least one server listed, the default one:&amp;nbsp;&lt;code&gt;mcp-adapter-default-server&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Notes:&lt;/strong&gt; Only abilities with&amp;nbsp;&lt;code&gt;meta.mcp.public =&amp;gt; true&lt;/code&gt;&amp;nbsp;are exposed through the MCP Adapter. This prevents private abilities from becoming available accidentally.&lt;/p&gt;

&lt;p&gt;Step 4.2: Connect Claude Desktop to your site&lt;/p&gt;

&lt;p&gt;Open Claude Desktop's configuration file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;macOS&lt;/strong&gt;:&amp;nbsp;~/Library/Application Support/Claude/claude_desktop_config.json&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Windows&lt;/strong&gt;:&amp;nbsp;%APPDATA%\Claude\claude_desktop_config.json&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fem646ocn7mlrrsfgz5yf.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fem646ocn7mlrrsfgz5yf.webp" alt="Edit Config File in Claude to Connect with Website Using MCP" width="799" height="525"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Add the following configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"mcpServers"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"my-wordpress-site"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"args"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"@automattic/mcp-wordpress-remote@latest"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="s2"&gt;"env"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"WP_API_URL"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"https://your-site.com/wp-json/mcp/mcp-adapter-default-server"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"WP_API_USERNAME"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"your-username"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"WP_API_PASSWORD"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"xxxx xxxx xxxx xxxx"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the following values with your own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WP_API_URL&lt;/strong&gt;: Replace&amp;nbsp;your-site.com&amp;nbsp;with your own site's actual address.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WP_API_USERNAME&lt;/strong&gt;: A WordPress user with permission to execute the ability.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WP_API_PASSWORD&lt;/strong&gt;: An Application Password for that user.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;New to Application Passwords?&amp;nbsp;Jump to the walkthrough at the end of this guide.&lt;/p&gt;

&lt;p&gt;Save the file and fully restart Claude Desktop (quit and reopen, not just close the window).&lt;/p&gt;

&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Important: &lt;/strong&gt;Claude runs abilities as whichever user's credentials you provided above. Your permission callbacks are checked against that exact user. This is also why permission callbacks matter so much, they're enforced on every single call, even from AI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4.3: Run Your Ability From Claude
&lt;/h3&gt;

&lt;p&gt;Open a new chat in Claude Desktop. If everything is configured correctly, your WordPress server appears in the available tools list.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9tir2w8697i85ghok3si.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9tir2w8697i85ghok3si.webp" alt="Website Connected to Claude" width="800" height="643"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now ask Claude:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Show me my five latest draft posts."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Discover the available abilities.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Select&amp;nbsp;&lt;code&gt;wpvibes/get-posts&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fill in the required input values.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ask for your approval.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Execute the ability.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Display the returned posts.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try a few more:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;"How many pages does my site have?"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;"List my latest WooCommerce products" (if you have a&amp;nbsp;&lt;code&gt;product&lt;/code&gt;&amp;nbsp;post type)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;"Do I have any posts scheduled for the future?"&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice something important: you never told Claude how to use your ability. The input schema did all the explaining. This is why we spent time writing good descriptions and schemas in Step 1, they aren't just documentation, they're instructions that AI reads and follows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE (security, read this twice):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Never paste API credentials directly into an AI chat prompt. Credentials belong in the configuration file, where the MCP client handles them, the AI model itself never needs to see them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use HTTPS only for remote connections.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create a dedicated user with only the capabilities your abilities need, don't use your main admin account.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Start with read-only abilities (like our Get Posts example). Add write or delete abilities only after you're comfortable with how everything works.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Application Passwords require "pretty" permalinks. If the MCP URL returns a 404, check&amp;nbsp;&lt;strong&gt;Settings → Permalinks&lt;/strong&gt;&amp;nbsp;isn't set to "Plain."&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Two More Real-World Abilities
&lt;/h2&gt;

&lt;p&gt;Now that you know the full method, here are two more abilities to add to the same plugin. The steps are the same as Example 1, so we'll only explain what's different in each one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 2: Add Missing Descriptions to Tags and Categories
&lt;/h3&gt;

&lt;p&gt;Most WordPress sites accumulate tags and categories over time, but descriptions often get skipped when they're created quickly. This matters more than it looks, category and tag archive pages often display that description publicly, and a missing one is a small but real SEO and content-quality gap. This example is also safe to try, since editing a description can always be changed again, nothing gets permanently deleted.&lt;/p&gt;

&lt;p&gt;This example uses two abilities together: one that finds the problem, and one that fixes it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Ability 2a: Find tags/categories with no description
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;What this code does&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This ability only looks, it never changes anything. It checks whichever taxonomy you tell it to (tags or categories), goes through every single term in it, and picks out only the ones where description is empty. It hands back a simple list, just the ID, name, and taxonomy for each one. Since nothing gets written or deleted here, Claude will run this one immediately without hesitation, exactly like the read-only &lt;code&gt;get-posts&lt;/code&gt; example from earlier in this guide.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wp_abilities_api_init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes_register_list_terms_without_description_ability'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpvibes_register_list_terms_without_description_ability&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nf"&gt;wp_register_ability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'wpvibes/list-terms-without-description'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'label'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'List Tags/Categories Without a Description'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-abilities'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Returns a list of tags or categories that currently have no description set.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-abilities'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'category'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-tools'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

            &lt;span class="s1"&gt;'input_schema'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'properties'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s1"&gt;'taxonomy'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="s1"&gt;'type'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Which taxonomy to check.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'enum'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'category'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'post_tag'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="s1"&gt;'default'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'category'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="s1"&gt;'output_schema'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'array'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'items'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s1"&gt;'type'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'properties'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="s1"&gt;'id'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'integer'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="s1"&gt;'name'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="s1"&gt;'taxonomy'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="s1"&gt;'execute_callback'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes_list_terms_without_description_callback'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

            &lt;span class="s1"&gt;'permission_callback'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;current_user_can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'manage_categories'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;

            &lt;span class="s1"&gt;'meta'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'mcp'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpvibes_list_terms_without_description_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nv"&gt;$taxonomy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'taxonomy'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;sanitize_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'taxonomy'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'category'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nv"&gt;$terms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_terms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'taxonomy'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$taxonomy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'hide_empty'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;is_wp_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$terms&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WP_Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'invalid_taxonomy'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'That taxonomy does not exist.'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$terms&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$term&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Only include terms with an empty or missing description.&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$term&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$results&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'id'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$term&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;term_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'name'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$term&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'taxonomy'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$term&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;taxonomy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$results&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ability 2b: Save a new description for one tag or category&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This ability handles one term at a time, and it needs three pieces of information to run: which term (&lt;code&gt;term_id&lt;/code&gt;), which taxonomy it belongs to, and the new description text to save. It first double-checks that the term actually exists, so it doesn't fail halfway through with a confusing error. Then it hands the new description off to WordPress's own built-in function, &lt;code&gt;wp_update_term()&lt;/code&gt;which saves just that one field. The term's name, its slug, everything else about it stays exactly as it was. Because this only changes a piece of text, and text can always be changed again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wp_abilities_api_init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes_register_update_term_description_ability'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpvibes_register_update_term_description_ability&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nf"&gt;wp_register_ability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'wpvibes/update-term-description'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'label'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Update Tag/Category Description'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-abilities'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Sets or updates the description for a specific tag or category.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-abilities'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'category'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-tools'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

            &lt;span class="s1"&gt;'input_schema'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'properties'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s1"&gt;'term_id'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="s1"&gt;'type'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'integer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'The ID of the tag or category to update.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="s1"&gt;'taxonomy'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="s1"&gt;'type'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Which taxonomy this term belongs to.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'enum'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'category'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'post_tag'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="s1"&gt;'type'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'The new description text to set.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="s1"&gt;'required'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'term_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'taxonomy'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="s1"&gt;'output_schema'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'properties'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s1"&gt;'term_id'&lt;/span&gt;         &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'integer'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="s1"&gt;'new_description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="s1"&gt;'execute_callback'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes_update_term_description_callback'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

            &lt;span class="s1"&gt;'permission_callback'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;current_user_can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'manage_categories'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;

            &lt;span class="s1"&gt;'meta'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'mcp'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpvibes_update_term_description_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nv"&gt;$term_id&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;absint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'term_id'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$taxonomy&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sanitize_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'taxonomy'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sanitize_textarea_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'description'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$term&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_term&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$term_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$taxonomy&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$term&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;is_wp_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$term&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WP_Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'term_not_found'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'That tag or category does not exist.'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_update_term&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nv"&gt;$term_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;$taxonomy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$description&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;is_wp_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'term_id'&lt;/span&gt;         &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$term_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'new_description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How you'd actually use it&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Ask Claude: "Which of my categories and tags don't have a description?" Claude runs Ability 2a and shows you the list.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ask Claude: "Write good descriptions for those and add them." Claude writes a description for each one itself, then runs Ability 2b once per category/tag to save it.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Tips:&lt;/strong&gt; This is a safe one to try for real. If a description comes out wrong, just ask Claude to write it again with different wording. Nothing here is permanent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 2: Site Maintenance Report (read-only, admin-only)
&lt;/h3&gt;

&lt;p&gt;Imagine asking Claude every Monday morning: "Give me my site's status report," and getting back pending updates, moderation queue, and health flags in one answer, instead of clicking through five admin screens.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wp_abilities_api_init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes_register_maintenance_report_ability'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpvibes_register_maintenance_report_ability&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nf"&gt;wp_register_ability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'wpvibes/maintenance-report'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'label'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Site Maintenance Report'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-abilities'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Returns a status report: versions, pending updates, comment queues, and basic health flags.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-abilities'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'category'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-tools'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

            &lt;span class="c1"&gt;// No input needed — an empty object schema.&lt;/span&gt;
            &lt;span class="s1"&gt;'input_schema'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'properties'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;stdClass&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="s1"&gt;'output_schema'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'properties'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s1"&gt;'environment'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="s1"&gt;'updates'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="s1"&gt;'content'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="s1"&gt;'health'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="s1"&gt;'execute_callback'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes_maintenance_report_callback'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

            &lt;span class="c1"&gt;// Stricter permission: admins only.&lt;/span&gt;
            &lt;span class="s1"&gt;'permission_callback'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;current_user_can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'manage_options'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;

            &lt;span class="s1"&gt;'meta'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'mcp'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpvibes_maintenance_report_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// Update counts need these includes outside of wp-admin context.&lt;/span&gt;
    &lt;span class="k"&gt;require_once&lt;/span&gt; &lt;span class="no"&gt;ABSPATH&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'wp-admin/includes/update.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;require_once&lt;/span&gt; &lt;span class="no"&gt;ABSPATH&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'wp-admin/includes/plugin.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nv"&gt;$update_data&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_get_update_data&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$comment_counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_count_comments&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$theme&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_get_theme&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'environment'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'wordpress_version'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;get_bloginfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'version'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'php_version'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;PHP_VERSION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'active_theme'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$theme&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Name'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$theme&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Version'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'site_url'&lt;/span&gt;          &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;home_url&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'updates'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'total_pending'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$update_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'counts'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'total'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'plugin_updates'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$update_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'counts'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'plugins'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'theme_updates'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$update_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'counts'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'themes'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'core_update'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$update_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'counts'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'wordpress'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'content'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'comments_awaiting_moderation'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$comment_counts&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;moderated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'spam_comments_in_queue'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$comment_counts&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;spam&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'draft_posts'&lt;/span&gt;                  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;wp_count_posts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'scheduled_posts'&lt;/span&gt;              &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;wp_count_posts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'health'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'https_active'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;is_ssl&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="s1"&gt;'debug_mode_on'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'WP_DEBUG'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="no"&gt;WP_DEBUG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What's new here:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stricter permission&lt;/strong&gt;:&amp;nbsp;&lt;code&gt;manage_options&lt;/code&gt;&amp;nbsp;(admins only). Version and update information is reconnaissance data; never expose it to low-privilege users.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Structured output&lt;/strong&gt;: we return grouped data (&lt;code&gt;environment&lt;/code&gt;,&amp;nbsp;&lt;code&gt;updates&lt;/code&gt;,&amp;nbsp;&lt;code&gt;content&lt;/code&gt;,&amp;nbsp;&lt;code&gt;health&lt;/code&gt;), not a pre-formatted text report. This gives AI clients the flexibility to present the information however they choose&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep callbacks fast&lt;/strong&gt;: we deliberately skip expensive checks like folder sizes or database size. AI agents may call abilities often; a slow ability makes every conversation slow.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once registered, you can ask Claude:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Give me my site's maintenance report and tell me what needs attention."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude calls the ability, analyzes the returned data, and explains what requires attention, for example, outdated plugins, a growing spam queue, or debug mode being enabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;You've now built your first WordPress ability, tested it, and connected it to Claude using the official MCP Adapter. From here, you can apply the same pattern to expose any WordPress functionality as a secure, reusable ability that works seamlessly with AI tools&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ on WordPress Abilities API
&lt;/h2&gt;

&lt;p&gt;What is the WordPress Abilities API?&lt;/p&gt;

&lt;p&gt;It's a core WordPress feature, added in version 6.9, that lets your site describe its capabilities in a standard format. Plugins, external tools, and AI agents can then discover and safely use those capabilities without custom, one-off integration code.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Do I need to understand MCP to use the Abilities API?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. You can register and use abilities entirely in PHP, or expose them over the REST API, without touching MCP at all. MCP is only needed if you specifically want an AI agent like Claude to discover and call your ability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use an ability without connecting it to any AI tool?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Abilities work from PHP, the REST API, and JavaScript, independent of AI. Many abilities are built purely so other plugins or your own code can call them through one consistent, permission-checked interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the MCP Adapter part of WordPress core?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No, it's a separate official plugin. WordPress deliberately keeps it outside core so abilities remain protocol-independent. If MCP is ever replaced by something newer, your registered abilities keep working unchanged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between an ability and a REST API endpoint?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A REST endpoint is one specific way to reach functionality over HTTP. An ability is broader: register it once, and it becomes callable from PHP, REST, JavaScript, and MCP, all from a single definition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do abilities only work with Claude, or other AI tools too?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Any MCP-aware AI client works, including Claude Desktop, Claude Code, Cursor, and VS Code. The ability itself doesn't know or care which AI is calling it, that's the whole point of the standard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://developer.wordpress.org/apis/abilities-api/" rel="noopener noreferrer"&gt;Abilities API: official Common APIs Handbook entry&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/WordPress/mcp-adapter" rel="noopener noreferrer"&gt;MCP Adapter on GitHub&lt;/a&gt;: docs, releases, and transport guides&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol specification&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to Create a WordPress Application Password
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Go to&amp;nbsp;&lt;strong&gt;Users → Profile&lt;/strong&gt;&amp;nbsp;(or&amp;nbsp;&lt;strong&gt;Users → All Users&lt;/strong&gt;&amp;nbsp;→ click your name).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scroll to&amp;nbsp;&lt;strong&gt;Application Passwords&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Type a name for it, something like "Claude MCP," so you remember what it's for.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click&amp;nbsp;&lt;strong&gt;Add New Application Password&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Copy the generated password immediately. WordPress shows it only once. If you lose it, you'll need to generate a new one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use this password, not your regular login password, wherever the guide asks for&amp;nbsp;&lt;code&gt;WP_API_PASSWORD&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fshggcrp170xubjrzwstv.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fshggcrp170xubjrzwstv.webp" alt="Create Application Password in WordPress" width="800" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>mcp</category>
      <category>abilitiesapi</category>
      <category>claude</category>
    </item>
    <item>
      <title>Complete Guide to WordPress Patterns: Synced, Unsynced, and Partial Sync</title>
      <dc:creator>WPVibes</dc:creator>
      <pubDate>Thu, 23 Jul 2026 06:31:01 +0000</pubDate>
      <link>https://dev.to/wpvibes/complete-guide-to-wordpress-patterns-synced-unsynced-and-partial-sync-1edd</link>
      <guid>https://dev.to/wpvibes/complete-guide-to-wordpress-patterns-synced-unsynced-and-partial-sync-1edd</guid>
      <description>&lt;p&gt;If you've ever wondered what the difference is between synced and unsynced WordPress patterns, the short answer is simple: synced patterns update everywhere, while unsynced patterns only affect the copy you're editing.&lt;/p&gt;

&lt;p&gt;But that's only part of the story. WordPress also supports &lt;strong&gt;Pattern Overrides&lt;/strong&gt;, a feature that enables &lt;strong&gt;partial sync&lt;/strong&gt;. It lets you keep a pattern's design and structure synced across your site while allowing specific content, such as text or images, to be edited independently in each instance.&lt;/p&gt;

&lt;p&gt;In this guide, you'll learn how synced and unsynced patterns work, how Pattern Overrides enable partial sync, when to use each approach, and how to build reusable patterns step by step.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are Patterns in WordPress?
&lt;/h2&gt;

&lt;p&gt;A pattern is a saved group of blocks you can insert anywhere on your site without rebuilding it from scratch. Think of it as a template for a recurring piece of content: a call-to-action, a pricing table, or a notice box.&lt;/p&gt;

&lt;p&gt;Every pattern you create in WordPress falls into one of two categories, and WordPress asks you to choose which one the moment you save it. Getting that choice right up front saves you from a rebuild later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is a Synced Pattern in WordPress?
&lt;/h2&gt;

&lt;p&gt;A synced pattern stays identical everywhere it's used. Edit it in one place, and that edit applies to every instance across your site automatically. This is the feature formerly known as a reusable block, renamed to synced patterns starting in WordPress 6.3, and it behaves the same way it always did: one source, many locations, always matching.&lt;/p&gt;

&lt;p&gt;Synced patterns make sense for anything that needs to stay consistent no matter where it appears: business hours, a newsletter signup bar, a standard disclaimer, or a footer contact block. Update the phone number once, and it's correct everywhere it's used.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do You Create a Synced Pattern?
&lt;/h2&gt;

&lt;p&gt;Let's build a real example together: a CTA box, the kind you'd use to promote a plugin or product at the end of a post.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Start a new pattern
&lt;/h3&gt;

&lt;p&gt;If you're starting fresh instead, go through the Appearance menu:&lt;/p&gt;

&lt;p&gt;If you use a block theme:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Go to Appearance &amp;gt; Editor &amp;gt; Patterns.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click Add pattern, then Create pattern.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you use a classic theme:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Go to Appearance &amp;gt; Design &amp;gt; Patterns.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click Add pattern, then Create pattern.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: Name it and keep Synced on
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Type a name, like "Plugin CTA - Standard."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Choose a category if you want one. This is optional.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keep the Synced option turned on.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click Create.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7wqvugj3atkr2ic30dxy.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7wqvugj3atkr2ic30dxy.webp" alt="Create Synced Pattern in WordPress" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Add blocks and design your pattern
&lt;/h3&gt;

&lt;p&gt;WordPress opens a blank canvas. This is where you actually build the pattern, and you can add any block you like here, not just the ones in this example.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Add a Group block. This holds the CTA together.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add your product logo, then insert a Heading block with a clear CTA, such as “Choose the Right Plan for You.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add a Paragraph block right below it for a short description, like "Manage everything from one dashboard."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add a Button block below that, with text like "View Pricing" and "Download Free", and a link to a product page.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Select the Group block, open Styles in the right panel, and add a background color and some padding, so it stands out as a promotional box.  &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk9cuh369945ffl1basu7.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk9cuh369945ffl1basu7.webp" alt="Design CTA with Synced WordPress Pattern" width="799" height="359"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Save the pattern
&lt;/h3&gt;

&lt;p&gt;Click Save in the top right corner. Your CTA pattern now exists.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Add it to a page so you can see it
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Open any post or page.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click the + button (block inserter) in the top left.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click the Patterns tab, then My Patterns.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click your CTA pattern. It gets added at your cursor.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click Save or Update. Your CTA now shows on the page, with a small purple icon marking it as synced.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc9uwe1e5emavlvdd6o3n.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc9uwe1e5emavlvdd6o3n.webp" alt="Add Synced Pattern on WordPress Page" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: See the sync in action
&lt;/h3&gt;

&lt;p&gt;Open your pattern again from &lt;strong&gt;Appearance &amp;gt; Editor &amp;gt; My Patterns&lt;/strong&gt;, and change the background color or button style.&lt;/p&gt;

&lt;p&gt;Go back to the page you just updated. The change has appeared there too, automatically, with no extra step. That's what "synced" actually means in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are Pattern Overrides (Partial Sync)?
&lt;/h2&gt;

&lt;p&gt;Here's the catch with a synced pattern: if you insert the same CTA on ten different posts, every single one promotes the exact same product, since editing it anywhere changes it everywhere. But usually, you want the box design locked while the heading, description, and button change depending on what you're promoting. That's exactly what overrides solve.&lt;/p&gt;

&lt;p&gt;An override marks one specific block inside a synced pattern as editable per instance. Everything else, the background color and padding, stays locked and synced. Only the content in the marked block can change from post to post.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Do You Enable Overrides on a Synced Pattern?
&lt;/h3&gt;

&lt;p&gt;Let's make the button inside our CTA editable on each page, both its text and its link, while keeping the box design locked everywhere.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Open your CTA pattern for editing (&lt;strong&gt;Appearance &amp;gt; Editor &amp;gt; My Patterns&lt;/strong&gt;, then click on it).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Select the Button block inside it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In the block settings panel, open the Advanced section.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Turn on Enable overrides, and give it a name like "CTA Button."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click Save.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4mn48n3dqwc59s1zzlh4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4mn48n3dqwc59s1zzlh4.webp" alt="Enable Pattern Override in WordPress" width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now open any page where you've added this CTA. Click directly into the button and change its text, then update its link to point to a different product. Only that page's button changes. The background color and padding stay exactly the same everywhere else, since only the button was marked as an override, not the styling.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is an Unsynced Pattern in WordPress?
&lt;/h2&gt;

&lt;p&gt;An unsynced pattern is a starting point, not a shared source. Once you insert it into a page or post, its text, links, and images are ready to edit right away, no extra steps needed. Editing one instance never affects any other place you've used the same pattern.&lt;/p&gt;

&lt;p&gt;Unsynced patterns are the better choice when you want a consistent layout but different content each time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Take an info box&lt;/strong&gt;: you might use the same background color and border across dozens of posts, but each one flags something different, a warning on one post, a quick tip on another, a "last updated" note on a third. Editing one shouldn't change the message on any of the others.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do You Create an Unsynced Pattern?
&lt;/h2&gt;

&lt;p&gt;The steps are almost identical to creating a synced pattern. This time, let's build an info box, since that's the example where each instance usually needs to say something different.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Start a new pattern
&lt;/h3&gt;

&lt;p&gt;Same as before, either from a page directly or through the Appearance menu (see Step 1 in the synced section above).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Name it and turn Synced off
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Type a name, like "Info Box - Flexible."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Choose a category if you want one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Turn the Synced option off this time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click Create.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwkyg3mgebtnq26y0d8ti.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwkyg3mgebtnq26y0d8ti.webp" alt="Create Unsynced Pattern in WordPress" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Add blocks and design your pattern
&lt;/h3&gt;

&lt;p&gt;WordPress opens a blank canvas, same as before.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Add a Group block. This holds everything together.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Inside the Group block, add a icon and Paragraph block, and type your message.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Select the Group block, open Styles in the right panel, and set a background color, some padding, and a left border. This is what makes it look like a proper info box instead of a plain paragraph.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5ym2l427krdpucpg0vla.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5ym2l427krdpucpg0vla.webp" alt="Create Info Box with Unsynced Pattern WordPress" width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Save the pattern
&lt;/h3&gt;

&lt;p&gt;Click Save.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Add it to a page so you can see it
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Open any post or page.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click the + button (block inserter).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click Patterns, then My Patterns.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click your info box pattern to insert it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click Save or Update. It shows up on the page. There's no purple icon this time, since it isn't synced.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click directly into the message text and change it. This works immediately, no extra step.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F06dopkprj06n61q0lfep.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F06dopkprj06n61q0lfep.webp" alt="Add Unsynced Pattern on Page" width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Add it to a second page with different text
&lt;/h3&gt;

&lt;p&gt;Repeat the same steps on another page, and type a completely different message. Both pages now show the same design, but different text, and neither page affects the other.&lt;/p&gt;

&lt;p&gt;Right now, this info box just shows a message. But say one page needs a "Learn More" link added right below the text, without changing the box everywhere else. That means adding a whole new block, not just changing text, so simple editing won't do it. Here's where Detach comes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do You Detach a Pattern?
&lt;/h2&gt;

&lt;p&gt;Detach unlocks full editing on one specific instance of a pattern, so you can add or remove blocks, something that plain text edits and Overrides can't do.&lt;/p&gt;

&lt;p&gt;Back to our info box example: you want to add a "Learn More" button right below the message, on just one page.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Select the inserted info box pattern on that page.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open the three-dot menu in the block toolbar.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Choose Detach.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhoepsyewgrajjs60r8eb.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhoepsyewgrajjs60r8eb.webp" alt="Detach Pattern in WordPress" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Add a new Button block right below the message, type "Learn More," and link it wherever it should go.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That one page now shows the message plus a button. Every other page using this same info box pattern still shows just the message, completely untouched.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhapuf0rkag649pkyicxf.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhapuf0rkag649pkyicxf.webp" alt="Added Button After Detaching Pattern" width="799" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This works the same way on a synced pattern, with one difference worth knowing. On an unsynced pattern like this info box, Detach is only needed for structural changes like this, since text was already editable without it.&lt;/p&gt;

&lt;p&gt;On a synced pattern like our CTA, nothing is editable directly at all, so Detach breaks the entire instance free at once, styling included, not just its content.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's the Real Difference Between Synced and Unsynced Patterns?
&lt;/h2&gt;

&lt;p&gt;The core difference between synced and unsynced patterns in WordPress comes down to one question: does an edit apply everywhere, or only to the instance you're editing? Synced patterns apply everywhere. Unsynced patterns apply only locally.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Synced Pattern&lt;/th&gt;
&lt;th&gt;Unsynced Pattern&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Editing behavior&lt;/td&gt;
&lt;td&gt;Updates every instance at once&lt;/td&gt;
&lt;td&gt;Updates only the instance you're editing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Content that must stay identical (footers, CTAs, disclaimers)&lt;/td&gt;
&lt;td&gt;Reusable layouts with changing content (info boxes, testimonials)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text edits&lt;/td&gt;
&lt;td&gt;Need Overrides enabled first&lt;/td&gt;
&lt;td&gt;Work immediately, no extra step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Block-level changes (add/remove)&lt;/td&gt;
&lt;td&gt;Need Detach&lt;/td&gt;
&lt;td&gt;Need Detach&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Library icon&lt;/td&gt;
&lt;td&gt;Purple icon in the pattern list&lt;/td&gt;
&lt;td&gt;No special icon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Shared content&lt;/td&gt;
&lt;td&gt;Reusable layouts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When Should You Use Synced vs Unsynced Patterns?
&lt;/h2&gt;

&lt;p&gt;Use a synced pattern when every page should show the exact same thing, like our CTA box. Use an unsynced pattern when each page needs different content in the same design, like our info box.&lt;/p&gt;

&lt;p&gt;If most of a pattern should stay locked but one small piece needs to change, reach for Overrides instead. If you need to add or remove a block on just one page, use Detach, on either pattern type.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQs on Synced and Unsynced WordPress Patterns
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between a synced and unsynced pattern in WordPress?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A synced pattern updates every instance across your site when you edit it. An unsynced pattern stays independent, so edits only affect the one instance you're working on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I convert an unsynced pattern to a synced pattern?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not directly, but Duplicate gives you a shortcut, it asks the sync setting again on the copy. Or build a new pattern from scratch and choose Synced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do pattern overrides work in WordPress?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Overrides let you mark one block inside a synced pattern as editable per instance. The rest of the pattern, including layout and styling, stays synced and locked everywhere else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need to detach an unsynced pattern just to edit its text?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Text, links, and images inside an unsynced pattern are editable the moment you insert it. Detach is only needed if you want to add or remove blocks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's a real-world example of when to use a synced pattern versus an unsynced pattern?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A CTA promoting the same offer everywhere is a good synced pattern. An info box with a different note on each post is a better fit for an unsynced pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Synced patterns keep things consistent, unsynced patterns keep things flexible, overrides let you edit content while keeping design locked, and Detach gives you full control over one instance when you truly need it. The CTA box and info box built above are small examples, but the same approach applies to nearly any repeating component on a WordPress site.&lt;/p&gt;

&lt;p&gt;Once reusable components like this are in place, the next challenge is usually keeping the surrounding content accurate over time, since sections can quietly go stale long after a post is published. That's the exact problem &lt;a href="https://wpvibes.com/plugin/content-lifecycle-manager/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=plugin_landing&amp;amp;utm_content=content_lifecycle_manager"&gt;Content Lifecycle Manager&lt;/a&gt; is built to solve, helping you track what needs a second look and when, so your site doesn't just look consistent, it stays accurate too.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>wordpresspatterns</category>
      <category>gutenberg</category>
      <category>reusableblocks</category>
    </item>
    <item>
      <title>Your Form Submissions Don't Have to Stay Locked in a Dashboard</title>
      <dc:creator>WPVibes</dc:creator>
      <pubDate>Wed, 22 Jul 2026 07:41:02 +0000</pubDate>
      <link>https://dev.to/wpvibes/your-form-submissions-dont-have-to-stay-locked-in-a-dashboard-5909</link>
      <guid>https://dev.to/wpvibes/your-form-submissions-dont-have-to-stay-locked-in-a-dashboard-5909</guid>
      <description>&lt;p&gt;Form submissions have a default destiny: land in a database, sit in an admin panel, get checked manually whenever someone remembers to look. That's fine for basic contact form enquiries. It's a missed opportunity for anything where the data itself has value beyond just being stored.&lt;/p&gt;

&lt;p&gt;A few situations where this shows up clearly:&lt;/p&gt;

&lt;p&gt;Someone's running event registrations and wants a live sign-up counter on the landing page - the kind that builds its own social proof as the number climbs. Someone else is collecting business or member listings through a form and needs a searchable directory, without spinning up a whole separate directory plugin just for that. Or a team needs to check incoming submissions without anyone getting a WordPress login.&lt;/p&gt;

&lt;p&gt;All three of these are the same underlying need: taking form data that's normally backend-only and putting it somewhere visible — on an actual page, updating itself, with zero manual maintenance.&lt;/p&gt;

&lt;h2&gt;
  
  
  So how do you actually put form data on a page?
&lt;/h2&gt;

&lt;p&gt;Normally, this requires custom development — querying a form plugin's database tables, building a template, handling pagination and filtering yourself. Not a huge lift for a developer, but not something most site owners can do themselves either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://formvibes.com/pricing/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=pricing&amp;amp;utm_content=form_vibes"&gt;Form Vibes Pro&lt;/a&gt;&lt;/strong&gt; has a feature called Data Profiles that handles this without any of that. Point it at a form, configure what to show and how to filter it, and it generates a shortcode (or an Elementor widget, if that's your builder) that renders live data anywhere on a page.&lt;/p&gt;

&lt;p&gt;Two display types cover most use cases:&lt;br&gt;
&lt;strong&gt;Table&lt;/strong&gt; — rows and columns, configurable per-column visibility and labels, optional pagination, and an optional live search bar so visitors can find a specific entry themselves rather than scrolling through everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Counter&lt;/strong&gt; — a single number reflecting how many entries match a given filter, with an optional message wrapped around it using dynamic tags (entry count, form name, plugin name). This is the registration-counter use case in its simplest form.&lt;/p&gt;

&lt;p&gt;Both pull directly from the Form Vibes database at render time — no caching lag, no manual refresh. A new submission shows up on the page the same way it shows up in the admin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filtering matters more than it sounds like it would
&lt;/h2&gt;

&lt;p&gt;The actual configuration worth paying attention to is the filtering layer, since that's what turns "dump all submissions on a page" into something genuinely useful.&lt;/p&gt;

&lt;p&gt;Filters work off form field values with a condition operator (contains, equals, etc.), and multiple conditions can be stacked with AND/OR logic. So a directory page can be scoped to "Category equals Web Design AND Status equals Approved" rather than showing every submission that's ever come in, including test entries and rejected listings.&lt;/p&gt;

&lt;p&gt;For a table specifically, there's also a separate row-expand option — click a row, see the full submission detail without needing every field crammed into the visible columns. Useful when a form has more fields than reasonably fit in a table view but the extra detail is still worth surfacing on demand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The internal-team-access angle
&lt;/h2&gt;

&lt;p&gt;One use case worth calling out specifically: giving a team visibility into submissions without WordPress login access at all.&lt;/p&gt;

&lt;p&gt;Combine a Data Profile table with WordPress's built-in page password feature, and you get a clean, searchable, password-gated view of submission data that anyone with the password can check — no WordPress account, no admin panel exposure, nothing beyond what's on that one page.&lt;/p&gt;

&lt;p&gt;This is a genuinely underused pattern. Client-facing or team-facing dashboards usually get built as custom internal tools; this gets most of the value from two built-in features stacked together. There's a full walkthrough of this specific setup here: &lt;strong&gt;&lt;a href="https://formvibes.com/tutorial/share-form-submissions-with-clients/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog_content&amp;amp;utm_content=form_vibes"&gt;sharing form submissions with clients&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  One honest limitation
&lt;/h2&gt;

&lt;p&gt;Data Profiles is Pro-only - not available on Form Vibes' free tier. Worth knowing before building a workflow around it, though for anything client-facing or public-facing, the alternative is usually custom development anyway, so the comparison point isn't really "&lt;strong&gt;&lt;a href="https://formvibes.com/general/form-vibes-free-vs-pro/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog_content&amp;amp;utm_content=form_vibes"&gt;free vs paid&lt;/a&gt;&lt;/strong&gt;," it's "paid feature vs hiring someone to build the same thing from scratch."&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>formsubmissions</category>
      <category>formvibes</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Stop Exporting Everything Just to Filter It Afterward</title>
      <dc:creator>WPVibes</dc:creator>
      <pubDate>Mon, 06 Jul 2026 06:10:16 +0000</pubDate>
      <link>https://dev.to/wpvibes/stop-exporting-everything-just-to-filter-it-afterward-3g5</link>
      <guid>https://dev.to/wpvibes/stop-exporting-everything-just-to-filter-it-afterward-3g5</guid>
      <description>&lt;p&gt;The default workflow for getting specific form submission data out of WordPress looks like this: export everything, open the spreadsheet, delete the rows you don't need, keep what you actually wanted.&lt;/p&gt;

&lt;p&gt;It works. But it means doing manual cleanup every single time. And if the same export is needed again next week — same form, same filters, same fields - the entire process repeats from scratch.&lt;/p&gt;

&lt;p&gt;There's a better approach: filter before exporting, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two export options in Form Vibes
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://wordpress.org/plugins/form-vibes/" rel="noopener noreferrer"&gt;Form Vibes&lt;/a&gt; has two ways to get submission data out of WordPress, and understanding which one fits which situation saves a lot of repetitive work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick Export&lt;/strong&gt; is free and available on every plan. Click the button in the submissions toolbar, and all entries for the currently selected form download as a CSV immediately. No configuration, no setup. Up to 1,000 entries per export. Good for one-off backups or full dataset downloads where you genuinely need everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Export Profiles&lt;/strong&gt; are a Pro feature and solve a different problem: recurring exports where the same configuration gets used again and again. Instead of downloading everything and filtering afterward, an Export Profile lets you define exactly what you want - which form, which date range, which field values to filter by, which columns to include - save that configuration once, and re-run it in one click any time you need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Export Profiles actually work
&lt;/h2&gt;

&lt;p&gt;The setup only needs to happen once per report or workflow. When creating a new Export Profile, four things get configured.&lt;/p&gt;

&lt;h2&gt;
  
  
  Profile Type
&lt;/h2&gt;

&lt;p&gt;Choose between two output options:&lt;br&gt;
&lt;strong&gt;Local Download&lt;/strong&gt; — exports submissions as a CSV file that downloads directly to your computer on demand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Sheets&lt;/strong&gt; — connects to a Google Spreadsheet via OAuth and pushes every new submission automatically in real time. Once set up, no manual export step is needed at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Source
&lt;/h2&gt;

&lt;p&gt;This is where you define exactly which submissions to include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
Form — select which form to pull submissions from&lt;/li&gt;
&lt;li&gt;
Date Range — choose a preset (Last Week, Last Month, Last Quarter) or set a custom window&lt;/li&gt;
&lt;li&gt;
Status — filter submissions by their current status: Read, Unread, or Spam&lt;/li&gt;
&lt;li&gt;
Filter Conditions — the most useful part. Add conditions based on any field value: select a field, choose a condition type (equals, contains, not equal, not contain), enter the value, and stack multiple conditions with AND or OR logic between them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So something like "Status equals Read AND Country equals USA AND Registration Type contains Paid" runs automatically every time the profile is used — no manual spreadsheet filtering afterward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fields&lt;/strong&gt;&lt;br&gt;
Choose which columns to include in the exported file. Toggle off anything that isn't needed for that specific export. A finance export might include Name, Email, Amount, and Date. A marketing export from the same form might include Name, Email, and Source only. Two profiles, two field selections, both in one click.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Profile Name&lt;/strong&gt;&lt;br&gt;
Worth being specific - "Monthly Contact Form Leads" or "Weekly Paid Registrations — USA" is far more useful than "Export 1" when the profiles list grows.&lt;/p&gt;

&lt;p&gt;Once published, the profile appears in the Export Profiles list. Click Export Data, confirm, and the file downloads based on saved settings. An export history inside each profile keeps a record of every previously generated file so past exports can be re-downloaded without running the export again.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this actually matters
&lt;/h2&gt;

&lt;p&gt;The filter-before-export approach pays off most noticeably in two situations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recurring reports&lt;/strong&gt;. If the same subset of data gets exported on a regular schedule — weekly leads, monthly paid registrations, quarterly support requests — setting it up once as a saved profile and running it in one click each time removes the repetitive configuration work entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Different subsets for different people&lt;/strong&gt;. Sales needs qualified leads. Marketing needs newsletter signups. Finance needs paid registrations. Support needs open requests. One form, four different export configurations, all saved and runnable independently without any of them having to touch settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  One honest limitation
&lt;/h2&gt;

&lt;p&gt;Export Profiles are a &lt;a href="https://formvibes.com/pricing/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=pricing&amp;amp;utm_content=form_vibes"&gt;Form Vibes Pro&lt;/a&gt; feature. The free Quick Export covers one-off full exports cleanly, but saved configurations with field-level filtering require Pro. Worth knowing upfront before building a workflow around it.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Why Form Submission Analytics Matters More Than a List of Entries</title>
      <dc:creator>WPVibes</dc:creator>
      <pubDate>Thu, 02 Jul 2026 05:46:49 +0000</pubDate>
      <link>https://dev.to/wpvibes/why-form-submission-analytics-matters-more-than-a-list-of-entries-35m7</link>
      <guid>https://dev.to/wpvibes/why-form-submission-analytics-matters-more-than-a-list-of-entries-35m7</guid>
      <description>&lt;p&gt;A contact form goes live. Submissions start coming in. Everything looks fine.&lt;/p&gt;

&lt;p&gt;But ask a more specific question - is this form doing better or worse than it was three months ago? - and most people genuinely can't answer it without manually counting entries across two different date ranges. A flat list of submissions tells you that something happened. It doesn't tell you the shape of what's happening over time.&lt;/p&gt;

&lt;p&gt;This is a gap in almost every form plugin's native entry view: storage without trend data.&lt;/p&gt;

&lt;h2&gt;
  
  
  What trend data actually catches that a list doesn't
&lt;/h2&gt;

&lt;p&gt;There are three patterns worth knowing how to spot, because each one means something different and calls for a different response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A sudden drop&lt;/strong&gt;. Steady submissions, then a sharp fall in a single week or month. This usually means something broke - a plugin update touched the form, the page moved to a new URL, or a layout change pushed the form out of place. The fastest way to confirm: submit a test entry yourself. If it goes through cleanly, the drop is likely traffic-related, not a form issue. If it doesn't submit correctly, that's the problem, found in under a minute instead of days of guessing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A slow decline&lt;/strong&gt;. No single bad week, just a gradual downward trend over weeks or months. This usually points to something upstream of the form itself - less traffic reaching the page, content on the page going stale, or something about the form discouraging completion. Looking at the trend week by week, rather than day by day, makes it easier to pin down roughly when the decline started, which helps connect it to whatever changed around that time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An unrepeated spike&lt;/strong&gt;. One period - a week, a month - significantly outperforms the baseline, then things return to normal. This is actually good news if you catch it: something drove a burst of traffic to the form. A campaign, a post that got shared, a backlink. Drilling into daily granularity for that window usually reveals exactly which day or days it happened, which makes it possible to figure out what worked and try to repeat it.&lt;/p&gt;

&lt;p&gt;None of these three patterns are visible from a list of individual entries. They only show up when submissions are aggregated over time and visualized.&lt;/p&gt;

&lt;h2&gt;
  
  
  So how do you actually check this?
&lt;/h2&gt;

&lt;p&gt;Most form plugins don't give you a trend view out of the box - just the entry list described earlier. Getting an aggregated view usually means exporting submissions and building the chart manually, or installing something that does the aggregation for you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://wordpress.org/plugins/form-vibes/" rel="noopener noreferrer"&gt;Form Vibes&lt;/a&gt; is a free WordPress plugin that captures submissions into the database and includes a built-in Analytics dashboard for exactly this. It works across every supported form plugin - Contact Form 7, WPForms, Gravity Forms, Elementor, and others with no separate setup per form, and capture starts automatically the moment the plugin is active.&lt;/p&gt;

&lt;p&gt;Analytics lives in two places:&lt;br&gt;
The Analytics page, found at &lt;strong&gt;Form Vibes → Analytics&lt;/strong&gt;, has three controls: a date range picker, a form dropdown (showing total entry count for that form at a glance), and a time filter to group submissions By Day, By Week, or By Month. &lt;/p&gt;

&lt;p&gt;Two chart types are available - bar and line, switchable from the same toolbar, and the last view (date range, form, chart type) is remembered automatically the next time the page is opened.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6tqav1nmqao5xeb4xbjl.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6tqav1nmqao5xeb4xbjl.webp" alt="Form Vibes Analytics" width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dashboard widget&lt;/strong&gt;, enabled from &lt;strong&gt;Form Vibes → Settings&lt;/strong&gt;, sits on the main WordPress admin screen - the first page seen on login. It shows a form dropdown, date range, time filter, a line chart, a total submission count, and an expandable breakdown of entries per form, all without navigating away from the dashboard.&lt;/p&gt;

&lt;p&gt;Both are available on the free plan with no Pro upgrade required.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftefn6yprx9oixalkdej0.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftefn6yprx9oixalkdej0.webp" alt="Form Vibes Dashboard Widget" width="370" height="695"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two chart types, two different questions
&lt;/h2&gt;

&lt;p&gt;Bar charts and line charts of the same data answer slightly different questions, and it's worth being deliberate about which one to reach for.&lt;/p&gt;

&lt;p&gt;A bar chart is for period-to-period comparison - was this month better than last month, did a specific campaign move the number. Each bar is a discrete answer to "how many in this window."&lt;/p&gt;

&lt;p&gt;A line chart is for direction - is the overall trajectory up, down, or flat. A line makes a slow decline visible in a way a row of bars sometimes doesn't, because the eye naturally reads a slope.&lt;/p&gt;

&lt;p&gt;Switching between daily, weekly, and monthly granularity changes what each chart type is good for too. Monthly view is for spotting the big picture across a quarter or more. Daily view is for pinpointing exactly when something changed once you already suspect something did.&lt;/p&gt;

&lt;h2&gt;
  
  
  One honest limitation worth knowing upfront
&lt;/h2&gt;

&lt;p&gt;Submission analytics measures submissions, not visits. It will tell you that 40 people filled out the form this month versus 60 last month. It will not tell you how many people loaded the page and left without submitting - that's a different metric (conversion rate) that requires pairing form analytics with actual page-view data from something like GA4.&lt;/p&gt;

&lt;p&gt;This matters because a drop in submissions has two very different possible causes: fewer people are reaching the form, or the same number of people are reaching it but fewer are completing it. Submission-only analytics can't distinguish between those two on its own - it just tells you the volume changed and roughly when, which is still the necessary first signal even without the full picture.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>formanalytics</category>
      <category>wordpressforms</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Two Ways to Give a Client Access to Their Form Data Without a WordPress Login</title>
      <dc:creator>WPVibes</dc:creator>
      <pubDate>Mon, 29 Jun 2026 06:30:06 +0000</pubDate>
      <link>https://dev.to/wpvibes/two-ways-to-give-a-client-access-to-their-form-data-without-a-wordpress-login-29e1</link>
      <guid>https://dev.to/wpvibes/two-ways-to-give-a-client-access-to-their-form-data-without-a-wordpress-login-29e1</guid>
      <description>&lt;p&gt;A client asks to see their form submissions. The solution is usually one of two things: create them a WordPress account, or export the data and email it over.&lt;/p&gt;

&lt;p&gt;Both have real downsides. A WordPress login even on a restricted role exposes the dashboard, plugin notices, update alerts, and a dozen things that have nothing to do with their actual request. Non-technical clients end up generating support questions just from being inside an interface they don't need to be in.&lt;/p&gt;

&lt;p&gt;Manual exports solve the access problem but create a different one: every time a new submission comes in, you're back to exporting and sending a file. It's not hard, but it's a task that never really ends, and "is this the latest version?" becomes a recurring question on both sides.&lt;/p&gt;

&lt;p&gt;There's a cleaner way to handle this that avoids both problems entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1 - A dedicated page the client can bookmark
&lt;/h2&gt;

&lt;p&gt;Instead of WordPress access, give the client a single URL that shows only their submissions, live, with no login required.&lt;/p&gt;

&lt;p&gt;The setup, generalized: create a filtered view of the form data scoped to that specific client or form, render it as a table on a normal WordPress page, hide any internal-only fields (IP address, user agent, raw submission IDs - nothing a client needs to see), and publish the page.&lt;/p&gt;

&lt;p&gt;Share the URL directly with the client rather than linking it from your site's navigation. The page stays publicly accessible to anyone with the link, so treat the URL itself as the access control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2 - Sync to a Google Sheet and share it as Viewer
&lt;/h2&gt;

&lt;p&gt;If the client already lives in spreadsheets, skip the custom page entirely and sync submissions directly to a Google Sheet.&lt;/p&gt;

&lt;p&gt;The setup: connect a Google account, configure an export that maps form fields to spreadsheet columns, and point it at a specific tab. Every new submission pushes into the sheet automatically from that point forward.&lt;/p&gt;

&lt;p&gt;Share the sheet with the client's email and set their permission to Viewer, not Editor - this is the part worth getting right. Viewer access means they can read, sort, and filter the data on their end without any risk of someone accidentally deleting or overwriting a row.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one actually fits your situation
&lt;/h2&gt;

&lt;p&gt;Go with the dedicated page when: you want something that looks like part of your site rather than a raw spreadsheet, the client isn't comfortable in Sheets, you need tight control over exactly which fields are visible, or you want built-in search for a high-volume form.&lt;/p&gt;

&lt;p&gt;Go with Google Sheets when: the client already works in spreadsheets daily, you want to set it up once and never touch it again, or the client needs to do their own filtering/sorting/sharing without depending on you for anything beyond initial setup.&lt;/p&gt;

&lt;p&gt;Nothing stops you from running both at once for the same form - one as the primary client-facing view, the other as an internal backup or reporting source. They operate independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in practice
&lt;/h2&gt;

&lt;p&gt;Both of these are built into &lt;a href="https://formvibes.com/pricing/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=pricing&amp;amp;utm_content=form_vibes"&gt;Form Vibes Pro&lt;/a&gt; - the page-based view is called a Data Profile, and the spreadsheet sync is a Google Sheets Export Profile. Neither requires touching code or setting up a separate integration tool; both are configured from inside WordPress and connect to forms from any of the major form plugins.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>formvibes</category>
      <category>googlesheets</category>
      <category>googlesheetsintegration</category>
    </item>
    <item>
      <title>How to Build a Form Submission Review Workflow for Your WordPress Team</title>
      <dc:creator>WPVibes</dc:creator>
      <pubDate>Thu, 25 Jun 2026 08:33:55 +0000</pubDate>
      <link>https://dev.to/wpvibes/how-to-build-a-proper-form-submission-review-workflow-for-your-wordpress-team-2ibo</link>
      <guid>https://dev.to/wpvibes/how-to-build-a-proper-form-submission-review-workflow-for-your-wordpress-team-2ibo</guid>
      <description>&lt;p&gt;When a form submission arrives, someone on the team needs to review it, act on it, and make sure nothing falls through. If the current process is "it lands in an inbox and whoever sees it first deals with it" - the failure modes are predictable. Leads get missed. The same entry gets followed up twice. A client asks why nobody responded and there is no record of what happened.&lt;/p&gt;

&lt;p&gt;This is not a tooling problem specific to any one form plugin. It exists with Contact Form 7, WPForms, Gravity Forms, or any other builder, because form plugins are built to collect data, not to manage it as a team. The submissions screen in most of them is a flat list with no workflow layer on top.&lt;/p&gt;

&lt;p&gt;Here is what a proper team submission workflow needs, and how to build one inside WordPress.&lt;/p&gt;

&lt;h2&gt;
  
  
  What breaks without a team workflow
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No visibility into what has been handled&lt;/strong&gt;. Every submission looks identical whether it arrived five minutes ago or was actioned three days ago. Team members have no way to tell without opening each entry individually or asking a colleague directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No way to leave context for the next person&lt;/strong&gt;. If you call a lead back and leave a voicemail, that context lives in your memory or a separate note-taking tool - disconnected from the actual submission record. The next person who opens that entry has no idea what already happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No record of who changed what&lt;/strong&gt;. If a field value gets edited, there is no audit trail showing who made the change or when. For any site where more than one person edits data, this is a real accountability gap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No controlled access for different people&lt;/strong&gt;. A junior team member reviewing submissions probably should not have delete access. A client wanting to see their own leads should not have full WordPress admin. Without a permission system specific to submission data, it is all-or-nothing access for everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the workflow piece by piece
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Status tracking for at-a-glance visibility&lt;/strong&gt;&lt;br&gt;
Every new submission should arrive in a default "needs review" state, visually distinct from entries that have already been actioned. As team members work through submissions, they update the state - read, unread, or flagged as irrelevant.&lt;/p&gt;

&lt;p&gt;The practical value of this is that anyone on the team can filter to "unread" or "pending" and immediately see exactly what needs attention, without having to ask a colleague or scroll through entries that have already been handled. For a two-person team reviewing daily enquiries, this single change removes the recurring "did you see this one?" message entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Internal notes attached to each entry&lt;/strong&gt;&lt;br&gt;
Status alone tells you whether something has been actioned. It does not tell you what was done. A note field attached directly to each submission solves this - "Called back, left voicemail, following up Friday", "Quote sent, awaiting response", "Duplicate of an earlier entry, do not action again."&lt;/p&gt;

&lt;p&gt;The notes should be completely internal, never visible to the person who submitted the form, and timestamped so the sequence of actions is clear when multiple people are involved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Role-based access control&lt;/strong&gt;&lt;br&gt;
Not everyone working with submission data needs the same permissions. A useful framework breaks access into four tiers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
View-only access for clients who need to see their own leads without any ability to change or delete data&lt;/li&gt;
&lt;li&gt;
Review access for junior team members - view entries, add notes, update status, but no export or delete&lt;/li&gt;
&lt;li&gt;
Full working access for account managers - everything above plus export capability&lt;/li&gt;
&lt;li&gt;
Complete access for senior team members or administrators
Define these tiers once per role and they apply automatically to every user assigned that role.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;An audit trail for every action&lt;/strong&gt;&lt;br&gt;
Every edit to a submission and every export action should be logged automatically - who made the change, what was altered, and when. This matters in two specific situations: when a submission appears to have been edited and you need to know what the original value was, and when an export was run and you want to know why. Logging an "export reason" alongside the username and timestamp turns a simple action log into a genuinely useful accountability record.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Form Vibes implements this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://formvibes.com/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=home&amp;amp;utm_content=form_vibes"&gt;Form Vibes Pro&lt;/a&gt;&lt;/strong&gt; builds exactly this workflow into WordPress.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Submission Status&lt;/strong&gt; marks every new entry as Unread by default, turning green when marked Read after review, or red when flagged as Spam. Filtering the dashboard to Unread at the start of a session shows exactly what needs attention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Notes&lt;/strong&gt; attach permanently to individual entries, visible only to users with Form Vibes access, never to the form submitter. Multiple team members can add separate timestamped notes to the same entry, building a clear history of what happened and when.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Role Manager&lt;/strong&gt; gives granular control over four WordPress user roles - Editor, Author, Contributor, and Subscriber - toggling permissions independently across submissions, notes, status, and exports. A Subscriber role with view-only access works well for client logins; a Contributor role with notes and status access but no delete works well for junior team members.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Activity Log&lt;/strong&gt; records every field edit and export action automatically, including an Export Reason prompt that captures why an export was run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;None of this requires migrating away from your existing form plugin. Submission Status, Notes, and Role Manager work as a layer on top of Contact Form 7, WPForms, Gravity Forms, Elementor, and other supported builders - no changes to your existing forms.&lt;/p&gt;

&lt;p&gt;For a two-person team currently relying on "whoever sees it first handles it," even just status tracking and notes removes most of the daily back-and-forth. Role-based access becomes worth setting up once clients or junior team members need their own access without full WordPress admin.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>formvibes</category>
      <category>wordpressforms</category>
      <category>formmanagement</category>
    </item>
    <item>
      <title>A Page Can Rank Well for Years Then Quietly Start Losing Ground. Here's Why.</title>
      <dc:creator>WPVibes</dc:creator>
      <pubDate>Tue, 23 Jun 2026 06:20:31 +0000</pubDate>
      <link>https://dev.to/wpvibes/a-page-can-rank-well-for-years-then-quietly-start-losing-ground-heres-why-5dhf</link>
      <guid>https://dev.to/wpvibes/a-page-can-rank-well-for-years-then-quietly-start-losing-ground-heres-why-5dhf</guid>
      <description>&lt;p&gt;A visitor lands on your post. They notice it was last updated in 2021. They read a statistic from a study that's five years old. The screenshot shows an interface that doesn't match what they're looking at right now. They hit back and find a competitor's article instead.&lt;/p&gt;

&lt;p&gt;No error message. No broken link. Nothing technically wrong. Just content that's quietly stopped being trustworthy - and it happens across dozens of pages without ever showing up as a problem you can point to directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does outdated content actually hurt rankings?
&lt;/h2&gt;

&lt;p&gt;Yes - and the mechanism is more specific than "Google penalizes old content."&lt;/p&gt;

&lt;p&gt;Google uses a freshness signal called Query Deserves Freshness (QDF). When a topic is evolving - new tools, updated stats, changed best practices - QDF kicks in and newer content gets a ranking advantage over older pages, even if the older page has more backlinks and a longer track record.&lt;/p&gt;

&lt;p&gt;This means a page can hold its position for two years and then start sliding, with nothing on your end having changed. The content isn't broken. It hasn't been penalized. It's just been outpaced by something more current.&lt;/p&gt;

&lt;p&gt;This gradual decline has a name: content decay. It doesn't happen overnight - a page can sit at the same ranking for months before the slide begins, and by the time the traffic drop shows up in Search Console, significant ground is already lost. Recovering it takes meaningfully more effort than maintaining it would have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does stale content damage trust so fast?
&lt;/h2&gt;

&lt;p&gt;Visitors don't consciously audit your content for accuracy. They make trust decisions within seconds of landing on a page, based on signals: an old publication date, a citation from a source that no longer exists, a screenshot of a redesigned interface.&lt;/p&gt;

&lt;p&gt;Each of these signals says the same thing without saying it directly - nobody's looking after this anymore. And trust damage doesn't stay contained to one page. A visitor who doesn't trust what they're reading doesn't trust the business behind it either.&lt;/p&gt;

&lt;p&gt;The content types that erode trust fastest, in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
A "best plugins" roundup recommending tools that have since been discontinued or acquired&lt;/li&gt;
&lt;li&gt;
A tutorial with screenshots from two interface versions ago&lt;/li&gt;
&lt;li&gt;
A pricing page listing plans that no longer exist&lt;/li&gt;
&lt;li&gt;
A stats post citing a six-year-old study as current&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How does this actually break conversions?
&lt;/h2&gt;

&lt;p&gt;Outdated content doesn't just push visitors away — it introduces doubt at the exact moment someone needs to feel confident enough to act. One piece of visibly old information is often enough to make someone hesitate, and hesitation usually ends a conversion before it starts.&lt;/p&gt;

&lt;p&gt;Three scenarios where this breaks down most often:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product pages with stale pricing&lt;/strong&gt;. A visitor sees one price on the page and a different one at checkout. Confidence in the entire purchase process collapses, not just the price field.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tutorials that no longer match the current interface&lt;/strong&gt;. Someone follows step three, sees a completely different screen than the screenshot shows, and assumes the tutorial itself is wrong — not that it's just old.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Why use this tool" pages listing features the product has changed or removed&lt;/strong&gt;. Setting an expectation the product can no longer meet is worse than never having made the claim.&lt;/p&gt;

&lt;p&gt;There's a compounding effect here too. Visitors who land on outdated content and bounce immediately - no scroll, no click, no interaction - send a strong signal to Google that the page isn't satisfying search intent. That signal feeds back into the ranking algorithm and accelerates the decay that's already underway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does this affect how AI tools see your content too?
&lt;/h2&gt;

&lt;p&gt;Yes, and this is the part that's easy to miss. AI search tools — ChatGPT, Perplexity, Google's AI Overviews — actively prefer to cite content that's accurate, specific, and demonstrably current. They evaluate credibility signals before deciding what to cite: named sources, verifiable data points, recent dates, consistent entity naming.&lt;/p&gt;

&lt;p&gt;Content that references deprecated tools or outdated statistics fails these checks and gets passed over for a competitor's fresher article — meaning outdated content doesn't just lose search visibility, it loses AI visibility too.&lt;/p&gt;

&lt;p&gt;The useful part: updating a post once improves both. There's no separate "AI optimization" work required beyond keeping the content accurate and current. It's the same maintenance work with two channels benefiting from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What other damage does old content quietly cause?
&lt;/h2&gt;

&lt;p&gt;A few things that rarely show up as obvious errors, which is exactly what makes them dangerous:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Broken internal links and orphaned pages&lt;/strong&gt;. As a site evolves, URLs change and categories get reorganized. Old posts accumulate links pointing to destinations that no longer exist - frustrating for visitors, and wasted crawl budget for Google, since time spent following dead links is time not spent indexing new content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Actively misleading instructions&lt;/strong&gt;. A tutorial written for an old plugin version that someone tries to follow on a newer one doesn't just fail to help — it can lead to the wrong setting getting changed or something breaking on their own site. The visitor doesn't blame the outdated post. They blame the site.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Domain-wide credibility damage&lt;/strong&gt;. Google's quality assessment looks at E-E-A-T (Experience, Expertise, Authoritativeness, Trustworthiness) at the domain level, not just per-page. A pattern of outdated content across a site reduces perceived authority site-wide, not just on the specific pages that are stale.&lt;/p&gt;

&lt;h2&gt;
  
  
  How often should content actually get reviewed?
&lt;/h2&gt;

&lt;p&gt;There's no single universal answer, but every page should have a review date — not just a publish date. A practical framework based on content type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
News and trend posts - review every 3-6 months, they age fastest&lt;/li&gt;
&lt;li&gt;
Tutorials and how-tos - review whenever the tool or platform gets a major update, don't wait for a fixed schedule&lt;/li&gt;
&lt;li&gt;
Stats and data roundups - review at least annually, anything older than 2 years should be treated as suspect by default&lt;/li&gt;
&lt;li&gt;
Evergreen guides - review every 6-12 months for accuracy and new context&lt;/li&gt;
&lt;li&gt;
Product and pricing pages - review immediately after any actual change, these have direct conversion impact and can't wait&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real issue most sites face isn't a lack of care about accuracy - it's the absence of any system that tracks what needs reviewing and when. Without that prompt, content just sits, slowly becoming a liability instead of an asset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;Fixing this doesn't require a one-time audit that gets forgotten six months later. It requires every post having a review date, an owner, and a place where overdue content actually shows up — inside WordPress, attached to the content itself.&lt;/p&gt;

&lt;p&gt;Content Lifecycle Manager is a free WordPress plugin built for exactly this. It adds review scheduling, ownership, and a Needs Attention queue right into your WordPress dashboard. The free version is available on &lt;a href="https://wordpress.org/plugins/content-lifecycle-manager/" rel="noopener noreferrer"&gt;WordPress.org&lt;/a&gt;. For owner email reminders, an activity digest, and a dashboard widget, &lt;a href="https://wpvibes.com/plugin/content-lifecycle-manager/" rel="noopener noreferrer"&gt;Content Lifecycle Manager Pro&lt;/a&gt; adds that on top.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>seo</category>
      <category>performance</category>
      <category>contentmanagement</category>
    </item>
    <item>
      <title>How to Manage Elementor Form Submissions Efficiently in WordPress</title>
      <dc:creator>WPVibes</dc:creator>
      <pubDate>Mon, 22 Jun 2026 06:05:58 +0000</pubDate>
      <link>https://dev.to/wpvibes/how-to-manage-elementor-form-submissions-efficiently-in-wordpress-21a0</link>
      <guid>https://dev.to/wpvibes/how-to-manage-elementor-form-submissions-efficiently-in-wordpress-21a0</guid>
      <description>&lt;p&gt;Elementor Pro includes a built-in submissions panel that stores every form entry automatically. If you are using the Elementor Form widget to capture leads, contact requests, or enquiries - your submissions are already being saved without any additional setup.&lt;/p&gt;

&lt;p&gt;But storing submissions and managing them efficiently are two different things. This guide covers both - what Elementor handles natively, where its built-in panel works well, where it has limits, and how to build a more complete submission management workflow on top of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Elementor stores form submissions
&lt;/h2&gt;

&lt;p&gt;Every time someone submits an Elementor form, the entry is written to the WordPress database automatically. No additional plugin needed. No configuration required. The Collect Submissions action is enabled by default on new Elementor forms.&lt;/p&gt;

&lt;p&gt;To view stored submissions, go to Elementor &amp;gt; Submissions in your WordPress admin. Select a form from the dropdown and every captured entry appears in a list with field values, submission date, and basic status options.&lt;/p&gt;

&lt;p&gt;One important thing to check: make sure the Collect Submissions action is active in your form's Actions After Submit settings. If it was ever removed and only an Email action is active, submissions exist only as email notifications with no database backup.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Elementor's submissions panel does well
&lt;/h2&gt;

&lt;p&gt;For straightforward use cases, Elementor's native panel is capable enough:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Entry storage and viewing&lt;/strong&gt; - every submission is stored reliably and viewable from the WordPress admin. Field values, submission date, and the page the form was submitted from are all recorded.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Basic status marking&lt;/strong&gt; - entries can be marked as Read or Unread. Useful for a single person doing a quick daily review of new submissions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Basic filtering&lt;/strong&gt; - filter by date range and read status. Sufficient for low-volume forms where you need a quick overview.&lt;br&gt;
Email notifications - configure notification emails that fire on every submission with field values pulled in dynamically via merge tags.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Bulk actions&lt;/strong&gt; - select multiple entries and mark as read, mark as unread, or delete in one action.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a site with one or two forms and one person reviewing submissions occasionally, this covers the basics without needing anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Elementor's submissions panel has limits
&lt;/h2&gt;

&lt;p&gt;The limits become noticeable as volume grows, team size increases, or reporting requirements become more specific.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No analytics or trend data&lt;/strong&gt;&lt;br&gt;
The submissions panel is a list. There is no way to see whether a form is generating more or fewer submissions over time, no daily or weekly breakdown, and no comparison across forms. If someone asks "how many leads did we get last month compared to this month?" - the answer requires manually counting entries across two date ranges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No field-level filtering&lt;/strong&gt;&lt;br&gt;
Elementor's filtering covers date range and read status only. There is no way to filter by a specific field value - for example, all submissions where the Service field equals "Web Design", or all entries where the budget field is above a certain value. Finding a specific subset of entries requires scrolling through the full list manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No audit trail&lt;/strong&gt;&lt;br&gt;
If a team member edits a submission in Elementor's panel - corrects a typo, updates a field value, there is no record of it. No timestamp, no who-changed-what. For any site with more than one person touching submission data, this creates an accountability gap that grows over time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No cross-plugin unified view&lt;/strong&gt;&lt;br&gt;
If your site runs Contact Form 7, WPForms, Gravity Forms, or any other form plugin alongside Elementor, those submissions are managed in completely separate places. There is no single view covering all your forms regardless of which plugin built them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No way to add internal notes&lt;/strong&gt;&lt;br&gt;
There is no way to attach an internal comment to a specific entry - a follow-up reminder, a team annotation, a status update. That context lives in someone's head, a Slack message, or an email thread, disconnected from the submission record itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No frontend display options&lt;/strong&gt;&lt;br&gt;
Submission data stays in the WordPress admin. There is no built-in way to display it on the frontend of your site as a counter or a table - without a separate plugin or custom development.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No advanced export&lt;/strong&gt;&lt;br&gt;
Elementor has a basic export option. There are no saved export configurations, no field-level filters applied before export, and no automatic sync to Google Sheets or other external destinations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a more complete submission management workflow
&lt;/h2&gt;

&lt;p&gt;For sites where the limits above create real friction, Form Vibes connects with Elementor Forms automatically and extends the submission management workflow without replacing anything Elementor already does.&lt;/p&gt;

&lt;p&gt;Install Form Vibes alongside Elementor Pro and it starts capturing Elementor form submissions from that point forward. Your existing forms stay exactly as they are - no changes needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this adds to the workflow:
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Unified dashboard across all form plugins&lt;/strong&gt;&lt;br&gt;
All supported form plugins feed into one Form Vibes dashboard. If your site runs Elementor alongside Contact Form 7, WPForms, or Gravity Forms - everything appears in one place. Switch between forms using a single dropdown with the same interface regardless of which plugin built the form.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Field-level filtering&lt;/strong&gt;&lt;br&gt;
Filter submissions by any field value, stack multiple conditions, and narrow down to exactly the subset you need. Find all enquiries where the budget field is above a certain value, or all submissions from a specific campaign landing page, in seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analytics dashboard&lt;/strong&gt;&lt;br&gt;
Daily, weekly, and monthly submission volume for any form. See whether lead generation is trending up or down, spot spikes that correspond to campaigns, and answer "how did we do this month compared to last?" without manually counting entries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Event Log&lt;/strong&gt;&lt;br&gt;
Every edit and export action is recorded automatically with a timestamp and username. If a field value looks wrong, the log shows whether it was changed and what the original value was. Starts recording from the moment Form Vibes is activated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Submission Notes&lt;/strong&gt;&lt;br&gt;
Attach internal annotations to any entry - follow-up reminders, team comments, status updates. Visible to anyone with Form Vibes access. Never sent to the submitter. Keeps submission context attached to the record where it belongs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advanced export and Google Sheets sync&lt;/strong&gt;&lt;br&gt;
Quick Export downloads all entries for any form as a CSV immediately. Export Profiles (Pro) save any export configuration - form, date range, field filters, output columns and re-run it in one click. Google Sheets sync (Pro) pushes every new submission to a connected spreadsheet automatically the moment it arrives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend submission display&lt;/strong&gt;&lt;br&gt;
Data Profiles (Pro) lets you display submission data on any page using a shortcode or Elementor widget - live counters, searchable tables, or password-protected team views.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical workflow combining Elementor and Form Vibes
&lt;/h2&gt;

&lt;p&gt;A typical submission review workflow with both active looks like this:&lt;/p&gt;

&lt;p&gt;New submission arrives → Form Vibes captures it automatically → appears in Form Vibes dashboard as Unread → team member reviews it, marks as Read, adds a note ("Followed up 14 June") → next team member opens the same entry and sees the full context without asking what happened → entry exported to CSV or pushed to Google Sheets for reporting.&lt;/p&gt;

&lt;p&gt;Elementor handles the form building and the initial storage. Form Vibes handles everything after that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;Form Vibes is free to install from &lt;a href="https://wordpress.org/plugins/form-vibes/" rel="noopener noreferrer"&gt;WordPress.org&lt;/a&gt;. Install it alongside Elementor Pro, submit one test entry on any Elementor form, and go to &lt;strong&gt;Form Vibes → Submissions&lt;/strong&gt; to confirm everything is capturing correctly.&lt;/p&gt;

&lt;p&gt;Read the full setup guide with screenshots on &lt;a href="https://formvibes.com/tutorial/save-elementor-form-submissions/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog_content&amp;amp;utm_content=form_vibes"&gt;How to Save and Manage Elementor Form Submissions&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>elementor</category>
      <category>formsubmissions</category>
      <category>formvibes</category>
    </item>
    <item>
      <title>Why Your WordPress Content Audit Always Feels Like Starting From Zero</title>
      <dc:creator>WPVibes</dc:creator>
      <pubDate>Fri, 19 Jun 2026 06:24:05 +0000</pubDate>
      <link>https://dev.to/wpvibes/why-your-wordpress-content-audit-always-feels-like-starting-from-zero-ndm</link>
      <guid>https://dev.to/wpvibes/why-your-wordpress-content-audit-always-feels-like-starting-from-zero-ndm</guid>
      <description>&lt;p&gt;You decide it's time for a content audit. You open Posts &amp;gt; All Posts and look at the list of 150, 200, maybe 300 posts sitting there.&lt;/p&gt;

&lt;p&gt;Then it hits you: you have no idea which of these were looked at before. You can't remember what you decided last time. There's no record anywhere of what the previous audit found, what got changed, or what was left for later.&lt;/p&gt;

&lt;p&gt;So you start over. Again.&lt;/p&gt;

&lt;p&gt;This isn't a sign you're running audits badly. It's a structural gap in how WordPress works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does every audit feel like starting from scratch?
&lt;/h2&gt;

&lt;p&gt;Because it does start from scratch. WordPress keeps no record of which posts have been checked, what was decided, or when someone last looked at a piece of content. Every audit begins from the same blank slate as the one before it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An audit produces decisions, not just changes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the part that's easy to miss. Running a content audit isn't just about making edits — it's about making decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
"This post is fine. Leave it alone."&lt;/li&gt;
&lt;li&gt;
"This one needs new screenshots."&lt;/li&gt;
&lt;li&gt;
"This post is too short. Expand it."&lt;/li&gt;
&lt;li&gt;
"This topic isn't relevant anymore. Remove it."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The decisions are the actual output. The edits you make are just the downstream result.&lt;/p&gt;

&lt;p&gt;When those decisions aren't saved anywhere, the next person reviewing the same posts has to make all of them again — with zero idea what was already decided or why.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo2h7vomnwd67333g1w1h.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo2h7vomnwd67333g1w1h.jpg" alt="WordPress Post Dashboard with No Content Management Features Available" width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where does the previous audit's work actually go?
&lt;/h2&gt;

&lt;p&gt;Usually into a spreadsheet, a Notion doc, or a shared folder living outside WordPress. The moment it leaves WordPress, it starts drifting toward irrelevance.&lt;/p&gt;

&lt;p&gt;Here's the typical lifecycle: a spreadsheet gets built with columns for post title, URL, last reviewed date, decision, reviewer, and notes. It's thorough. The team is happy with it.&lt;/p&gt;

&lt;p&gt;Three months later, the person who built it has moved to other work. The spreadsheet hasn't been touched since the audit wrapped. Some rows still reflect the original decisions. Others were partially updated when a post changed. Nobody's sure which information is still accurate.&lt;/p&gt;

&lt;p&gt;Six months in, a new team member opens it to prep for the next cycle. Post titles don't match WordPress anymore. Some posts were deleted. Some merged. Some URLs changed. The spreadsheet is now a record of a content library that doesn't quite exist anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why even good records fail
&lt;/h2&gt;

&lt;p&gt;The problem isn't the quality of the record-keeping — it's the location. Any record living outside WordPress needs a human to manually keep it in sync every time something changes in the actual content. That synchronization step gets skipped constantly, not from carelessness, but because updating a spreadsheet after every content change is overhead layered on top of the real work — and overhead is what gets cut when time is tight.&lt;/p&gt;

&lt;p&gt;The result: the audit record drifts out of sync with reality faster than the content itself goes stale. By the next audit cycle, the external record is a mix of accurate and inaccurate information that can't be trusted without checking each item against the live site individually. At that point, starting from scratch genuinely is faster than reconciling the spreadsheet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What losing audit history actually costs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Time&lt;/strong&gt;. A site with around 100 posts often needs multiple days of manual review for a complete audit. If that investment produces no lasting record, the same multi-day investment happens again next cycle. Over three or four years of annual audits, that's weeks of duplicated work that didn't need to happen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strategic context&lt;/strong&gt;. Audit decisions often carry reasoning that isn't obvious just from looking at a post: "Keep this exactly as is — it targets a narrow query and performs well despite its age" or "Deliberately kept short because expanding it would shift search intent." When that reasoning disappears, the next reviewer either repeats a decision without knowing it was already made, or reverses it without knowing why it was made that way originally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Institutional knowledge&lt;/strong&gt;. When audit history lives only in people's memory, it leaves with them when they change roles or move on. The next person auditing the site isn't starting from scratch because the site is new — they're starting from scratch because nothing was ever captured anywhere that could survive a personnel change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this gets worse as your site grows
&lt;/h2&gt;

&lt;p&gt;At 20 posts, one person can hold a rough mental picture of the whole library. The lack of a formal record is uncomfortable but workable.&lt;/p&gt;

&lt;p&gt;At 100 posts, memory stops being reliable. A record becomes genuinely necessary — so the spreadsheet gets built, and starts drifting almost immediately.&lt;/p&gt;

&lt;p&gt;At 200-300 posts, the audit itself becomes daunting enough that teams delay it, then rush a compressed version when they finally do it, producing a record that's incomplete from day one. The next audit starts even further behind than the last.&lt;/p&gt;

&lt;p&gt;The gap between content library size and audit infrastructure widens every cycle unless something changes how the history is captured.&lt;/p&gt;

&lt;h2&gt;
  
  
  What preserved audit history would actually look like
&lt;/h2&gt;

&lt;p&gt;If audit decisions lived inside WordPress, an audit wouldn't start from scratch - it would start from the last known state of every post: who reviewed it, when, what they decided, and what's next.&lt;/p&gt;

&lt;p&gt;That means opening the dashboard and seeing, next to every post, when it was last reviewed — not edited, not published, but actually reviewed by a human who made a deliberate call about it. Sorting by review date instead of publish date, so you tackle the most neglected posts first instead of working through the list in an arbitrary order.&lt;/p&gt;

&lt;p&gt;A thirty-second note at the end of a review - "Screenshots updated in section three, step four needs checking when the next plugin version ships" — saves thirty minutes of context-rebuilding the next time someone opens that post.&lt;/p&gt;

&lt;p&gt;This doesn't require anything complicated. It just requires the audit record to live in the same place as the content it describes - attached to the actual posts, not floating in a document that drifts out of sync the moment it's created.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;None of this needs a custom build or an external tracking system bolted on top of WordPress. It just needs review dates, decisions, and notes stored directly alongside each post inside WordPress itself.&lt;/p&gt;

&lt;p&gt;Content Lifecycle Manager is a free WordPress plugin built for exactly this — review dates, audit decisions (healthy, needs attention, archive), owner notes, and next actions, all stored directly on each post so the work from one review cycle carries into the next. The free version is available on &lt;a href="https://wordpress.org/plugins/content-lifecycle-manager/" rel="noopener noreferrer"&gt;WordPress.org&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://wpvibes.com/plugin/content-lifecycle-manager/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=plugin_landing&amp;amp;utm_content=content_lifecycle_manager"&gt;Content Lifecycle Manager Pro&lt;/a&gt; adds owner email reminders, an admin activity digest, and a full activity log on top.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>contentmanagement</category>
      <category>contentaudit</category>
    </item>
    <item>
      <title>WordPress Publishes 70M+ Posts a Month. None of Them Get Tracked After That</title>
      <dc:creator>WPVibes</dc:creator>
      <pubDate>Fri, 19 Jun 2026 05:15:45 +0000</pubDate>
      <link>https://dev.to/wpvibes/wordpress-publishes-70m-posts-a-month-none-of-them-get-tracked-after-that-2hhp</link>
      <guid>https://dev.to/wpvibes/wordpress-publishes-70m-posts-a-month-none-of-them-get-tracked-after-that-2hhp</guid>
      <description>&lt;p&gt;You click Publish. WordPress confirms the post is live, gives you a link to share, and moves on.&lt;/p&gt;

&lt;p&gt;Nothing tracks who is responsible for that post going forward. Nothing tracks whether the information inside it is still accurate next month, next year, or three years from now. WordPress is excellent at the moment of publishing. What happens after that is a gap nobody really talks about.&lt;/p&gt;

&lt;h2&gt;
  
  
  What WordPress actually tracks after you hit publish
&lt;/h2&gt;

&lt;p&gt;Open Posts &amp;gt; All Posts. The columns you see are title, author, category, tags, and publish date.&lt;/p&gt;

&lt;p&gt;That's the complete picture WordPress gives you about any published post. None of those columns tell you whether the post is still accurate. None of them tell you who's responsible for keeping it that way. None of them tell you when it was last checked or when it should be checked again.&lt;/p&gt;

&lt;p&gt;According to WordPress.com's own activity data, WordPress users publish roughly 70 million posts a month — more than two million a day. Every one of them enters a platform with zero built-in mechanism to track whether it stays accurate over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this gap exists
&lt;/h2&gt;

&lt;p&gt;WordPress was built as a publishing platform in 2003. Writing, scheduling, and publishing all work smoothly. What WordPress was never built to do is manage content after it goes live.&lt;/p&gt;

&lt;p&gt;WordPress 6.9 added inline editor notes for team collaboration — a genuinely useful update. But it changed nothing about what happens to content after publishing. The gap is exactly where it was before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the common workarounds fail
&lt;/h2&gt;

&lt;p&gt;Most teams eventually notice this problem and try to patch it with external tools. Three attempts come up constantly, and all three break down for a similar reason.&lt;/p&gt;

&lt;p&gt;The spreadsheet. Someone builds a tracker with columns for post title, last reviewed, and owner. It works for a few weeks. Then someone updates a post and forgets to update the spreadsheet. The spreadsheet starts reflecting what the team intended to do, not what they actually did.&lt;/p&gt;

&lt;p&gt;Project management tools. Trello, Asana, Notion — same fundamental issue. They live outside WordPress, so keeping them in sync with actual content changes requires someone to manually update both places every time. That discipline rarely survives more than a month.&lt;/p&gt;

&lt;p&gt;The WordPress "Last Modified" date. This one is worth dwelling on because it looks like a solution but isn't. The Last Modified date updates on any save — fixing a typo, adding a tag, changing a category. A post showing "modified yesterday" could mean someone did a thorough accuracy review, or it could mean someone fixed one spelling mistake. There's no way to tell the difference from the date alone. Teams relying on it as a review signal are working off false confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually looks like in practice
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The tutorial nobody touched&lt;/strong&gt;. A two-year-old post still ranks on page one and gets daily visitors. The tool it explains has been redesigned twice since. The screenshots show interfaces that no longer exist. The steps now cause errors. Nobody flagged it — it just kept ranking and kept sending readers through broken instructions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The team member who left.&lt;/strong&gt; A writer published eight posts over six months, then moved to a different job. Their name is still listed as author on all eight. Nobody was assigned responsibility for that content when they left, because WordPress has no mechanism for reassigning it. Six months later, a reader flags an error in one of the posts. Nobody knows who should look at it or when it was last checked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The statistics post that kept ranking&lt;/strong&gt;. A post citing industry figures was accurate when published. Three of the original sources have since updated their numbers. The post still ranks and still displays the outdated figures as current. Readers checking the original sources find the discrepancy and lose trust in the site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why nobody notices until it's a problem
&lt;/h2&gt;

&lt;p&gt;WordPress gives no warning when a post goes stale — no flag, no alert, nothing in the dashboard. A post untouched for two years looks identical in the All Posts list to one reviewed last week.&lt;/p&gt;

&lt;p&gt;The first signs typically come from outside WordPress entirely: a reader email pointing out an error, a slow traffic decline in Search Console, a ranking drop after a Google algorithm update. By the time any of these surface, the problem has usually been compounding for months.&lt;/p&gt;

&lt;p&gt;Most readers who land on inaccurate content don't email to complain. They leave, and you never know they were there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually costs over time
&lt;/h2&gt;

&lt;p&gt;Three things degrade simultaneously, and all three are easy to miss because they happen gradually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Search rankings&lt;/strong&gt;. Search engines factor freshness into ranking for queries where recency matters. A page untouched for two years, competing against actively maintained content on the same topic, slides down slowly — easy to miss until the drop is already significant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reader trust&lt;/strong&gt;. A reader who hits an error mid-tutorial, or finds a cited statistic no longer matches the source, doesn't usually come back to give the post a second chance. A widely cited industry survey found that roughly a third of people say outdated content directly damages their trust in a brand. That trust doesn't reset just because the post eventually gets fixed — the reader who left has already left.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Team accountability&lt;/strong&gt;. Without a named owner and a review date, responsibility for any given post belongs to everyone in theory and no one in practice. When something does go wrong, there's no clear person to fix it and nothing stopping the same gap from opening up in another post a few months later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should actually happen after publishing
&lt;/h2&gt;

&lt;p&gt;Four things, none of them complicated:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A review date gets set&lt;/strong&gt; — distinct from the publish date. Publish date tells you when something went live. Review date tells you when someone should next verify it's still accurate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An owner gets named&lt;/strong&gt; — distinct from the author. The author wrote the post. The owner is responsible for keeping it accurate going forward. On a single-person site these are the same person. On any team, they often aren't, and without a named owner, nobody is actually in charge of a published post.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A next action gets specified&lt;/strong&gt;. "Someone should look at this eventually" isn't a next action. A real one looks like: verify accuracy and set a new review date, update a specific outdated section, archive if no longer relevant, or redirect to a newer version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An attention queue exists somewhere&lt;/strong&gt;. There's no native WordPress view showing which posts are due for review this month, which have no owner, or which have never been checked since publishing. Until something is added to provide that view, there's no way to see what needs attention next.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest takeaway
&lt;/h2&gt;

&lt;p&gt;None of this is solvable by trying harder at remembering. Spreadsheets and external trackers fail consistently because they live outside the system that actually holds the content — any tracking mechanism not connected directly to WordPress will eventually drift out of sync with reality.&lt;/p&gt;

&lt;p&gt;The sites that maintain rankings and reader trust over years are the ones that built a system for exactly this gap — review dates, named ownership, and a visible queue, living inside WordPress rather than in a spreadsheet that quietly falls behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;Fixing this doesn't mean switching platforms or stitching together a bunch of external tools. It just means giving every post a review date, an owner, and a place where overdue posts show up — all inside WordPress, attached to the actual content.&lt;/p&gt;

&lt;p&gt;Content Lifecycle Manager is a free WordPress plugin built for exactly this. It adds ownership, review dates, and a Needs Attention queue right into your WordPress dashboard. The free version is on WordPress.org. For email reminders, an activity log, and a dashboard widget, Content Lifecycle Manager Pro adds that on top.&lt;/p&gt;

&lt;p&gt;Content Lifecycle Manager is a free WordPress plugin built for exactly this. It adds ownership, review dates, and a Needs Attention queue right into your WordPress dashboard. The free version is available on &lt;a href="https://wordpress.org/plugins/content-lifecycle-manager/" rel="noopener noreferrer"&gt;WordPress.org&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;For email reminders, an activity log, and a dashboard widget, &lt;a href="https://wpvibes.com/plugin/content-lifecycle-manager/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=plugin_landing&amp;amp;utm_content=content_lifecycle_manager"&gt;Content Lifecycle Manager Pro&lt;/a&gt; adds that on top.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>contentmanagement</category>
    </item>
  </channel>
</rss>
