Become the best admin of your WordPress site

Use GraphQL for much more than headless!
- Modify data in bulk
- Automate tasks
- Send notifications
- Translate posts, block by block
- Sync content across sites
- Interact with external services
WordPress on the backend, your framework of choice in the client
Use Gato GraphQL to fetch data from WordPress, to power your headless or decoupled app, blocks, themes and plugins.


query { post(id: 1) { title author { name } } }
query { author(id: 5) { name posts { title } } }
query { tags(limit: 2) { name count } }
{ "data": { "post": { "title": "Welcome to my site", "author": { "name": "Sandra" } } } }
{ "data": { "author": { "name": "Adriano", "posts": [ { "title": "Prisencolinensinainciusol" } ] } } }
{ "data": { "tags": [ { "name": "news", "count": 3 }, { "name": "tutorials", "count": 6 } ] } }
Gato GraphQL is your personal Swiss Army knife to interact with your data
Satisfy the data requirements to maintain your site, executing one-off or repetitive admin tasks via GraphQL. Run your queries directly in the wp-admin, or trigger them via automation.
x
}
query GetPostAndExportData($postId: ID!) {
post(by: { id : $postId }) {
id
rawContent @export(as: "content")
rawExcerpt @export(as: "excerpt")
rawTitle @export(as: "title")
}
}
mutation DuplicatePost
@depends(on: "GetPostAndExportData")
{
createPost(input: {
contentAs: {
html: $content
},
excerpt: $excerpt
title: $title
}) {
status
errors {
__typename
...on ErrorPayload {
message
}
}
post {
id
content
excerpt
title
}
}
}
xxxxxxxxxx
xxxxxxxxxx
{
"data": {
"post": {
"id": 1,
"rawContent": "<!-- wp:paragraph -->\n<p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing!</p>\n<!-- /wp:paragraph -->",
"rawExcerpt": "",
"rawTitle": "Hello world!"
},
"createPost": {
"status": "SUCCESS",
"errors": null,
"post": {
"id": 1245,
"content": "\n<p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing!</p>\n",
"excerpt": "Welcome to WordPress. This is your first post. Edit or delete it, then start writing!",
"title": "Hello world!"
}
}
}
}
Combining the best from REST and GraphQL
Gato GraphQL provides Persisted Queries out of the box. Publish an endpoint under its own URL (like the REST API), but directly from the wp-admin (without any PHP code!)

One single tool to take care of countless needs.
Augment WordPress search capabilities
Sometimes searching for data within WordPress is limited, as with custom fields: Searching in WordPress for posts that contain some keyword will not search within meta values.
Gato GraphQL can complement these capabilities, allowing us to search for posts (and also users, comments, and taxonomies) by meta key and value (including via regex expressions).
Query dynamic data
Gato GraphQL provides "function" fields, allowing us to compute logic already within the GraphQL query, thus avoiding the need to code a client application.
This way, we can dynamically compute data, input it back into the query, and affect the response with granular control. For instance, we can compute the date "24hs ago" and search for comments added from this date onwards.
Complement WP-CLI
Gato GraphQL can empower WP-CLI, by helping us find the WordPress data we need with granular control, and then injecting into the WP-CLI command (to update a post or user, reply to a comment, delete an option, etc).
We can even retrieve the data from multiple resources at once, and execute WP-CLI on all of them.
For instance, we can select all users with any Spanish locale, and update their locale to Spanish from Argentina.
Send personalized emails
Gato GraphQL allows us to execute mutations under any type from the GraphQL schema (i.e. not only under the Root type).
We can then iterate the list of users, obtain their data (name, email and a meta value with the number of remaining credits), dynamically compose a message using Markdown, and send a personalized email to the user.
Power blocks with DRY logic (for CSR/SSR)
Rendering a dynamic (Gutenberg) block on the client (for the WordPress editor) or on the server-side (for printing the blog post) typically requires fetching the block's data in different ways: using JavaScript to connect to the API endpoint, and using PHP to call WordPress functions, respectively.
Gato GraphQL allows to have a single source of truth to fetch data for both the client and server-sides, making this logic DRY (Don't Repeat Yourself).
Map JavaScript components to blocks
Gato GraphQL provides fields to query the properties from (Gutenberg) blocks.
This allows us to use block data when building a headless (or decoupled) application, mapping each block with a custom JavaScript component.
Duplicate a blog post
Duplicating a post is an example of Gato GraphQL's ability to retrieve, manipulate and store again data in the site.
You have the ultimate control on how the post will be duplicated. Change the author or post status, append "(Copy)" to the title, or any other.
Customize content for different users
We can retrieve a different response in a field depending on some piece of queried data, such as the roles of the logged-in user.
For instance, the post content can append an "Edit this post" link at the bottom of the content for the admin user only.
Adapt content in bulk
Gato GraphQL allows us to execute a mutation on hundreds or even thousands of resources at once.
Migrate the domain, or a post or page slug, in all content
After migrating the site to a new domain, or changing the slug of a post or page, we can convert all content throughout the site to point to the new URL.
Insert/remove a block in bulk
We can update posts by modifying their (Gutenberg) block's HTML content, and we can do it in bulk.
This is useful for promoting campaigns, inserting a custom block with our Call To Action to all posts in the website, and removing it from everywhere right after the campaign ends.
Retrieve structured data from blocks
We can iterate the (Gutenberg) blocks in the post and extract the attributes from deep within the block structure, unlocking these attributes to be fed into any functionality in our site, and to be exposed via an API to power our other applications (mobile app, newsletter, etc).
For instance, by extracting all the image URLs contained in the core/image blocks in a post, we can create an image carousel with all these images.
Automate tasks
Gato GraphQL can help us automate tasks in the application, such as sending a notification email when a comment is added, automatically adding a mandatory block to a new post, pinging external services on some activity, and others.
Gato GraphQL can also be integrated with WP-Cron, allowing us to execute GraphQL queries that run some admin task on a timely basis.
Interact with external services
Gato GraphQL provides fields to execute HTTP requests against a webserver, allowing us to interact with external services and APIs.
For instance, we can retrieve the list of subscribers from a Mailchimp list, combine those records with the user data in the site, and execute an action with the augmented data.
Filter data from an external API
If the external API does not allow filtering by a certain property that we need, we can use Gato GraphQL to iterate over the entries in the API response, and remove those ones that do not satifsy our condition.
query {
postsWithThumbnail: posts( filter: {
metaQuery: {
key: "_thumbnail_id",
compareBy: {
key: { operator: EXISTS }
}
}
} ) {
id
title
featuredImage {
src
}
}
usersWithSpanishLocale: users( filter: {
metaQuery: {
key: "locale",
compareBy: { stringValue: {
operator: REGEXP
value: "es_[A-Z]+"
} }
}
} ) {
id
name
locale: metaValue(key: "locale")
}
}
query {
timeNow: _time
time24HsAgo: _intSubstract(
substract: 86400,
from: $__timeNow
)
date24HsAgo: _date(
format: "Y-m-d\\TH:i:sO",
timestamp: $__time24HsAgo
)
commentsAddedInLast24Hs: comments(
filter: {
dateQuery: {
after: $__date24HsAgo
}
}
) {
date
content
author {
name
}
}
}
GRAPHQL_QUERY='
query RetrieveData {
users( filter: {
metaQuery: {
key: "locale",
compareBy: { stringValue: {
value: "es_[A-Z]+"
operator: REGEXP
} }
}
} ) {
id @export(
as: "userIDs",
type: LIST
)
}
}
query FormatAndPrintData @depends(on: "RetrieveData") {
spanishLocaleUserIDs: _arrayJoin(
array: $userIDs,
separator: " "
)
}
'
GRAPHQL_BODY="{\"operationName\": \"FormatAndPrintData\", \"query\": \"$(echo $GRAPHQL_QUERY | tr '\n' ' ' | sed 's/"/\\"/g')\"}"
GRAPHQL_RESPONSE=$(curl -X POST -H "Content-Type: application/json" -d $GRAPHQL_BODY https://mysite.com/graphql/)
SPANISH_LOCALE_USER_IDS=$(echo $GRAPHQL_RESPONSE | grep -E -o '"spanishLocaleUserIDs\":"((\d|\s)+)"' | cut -d':' -f2- | cut -d'"' -f2- | rev | cut -d'"' -f2- | rev)
for USER_ID in $(echo $SPANISH_LOCALE_USER_IDS); \
do wp user update "$(echo $USER_ID)" --locale=es_AR; \
done
mutation {
users {
email
displayName
remainingCredits: metaValue(key: "credits")
emailMessageTemplate: _strConvertMarkdownToHTML(
text: """
Hello %s,
Your have **%s remaining credits** in your account.
Would you like to [buy more](%s)?
"""
)
emailMessage: _sprintf(
string: $__emailMessageTemplate,
values: [ $__displayName, $__remainingCredits, "https://mysite.com/buy-credits" ]
)
_sendEmail( input: {
to: $__email
subject: "Remaining credits alert"
messageAs: {
html: $__emailMessage
}
} ) {
status
}
}
}
/** JavaScript: Fetch data to render the block on the client (CSR) */
import dryGraphQLQuery from './graphql-documents/fetch-posts-for-my-block.gql';
const response = await fetch(endpoint, {
body: JSON.stringify({
query: dryGraphQLQuery
)
} );
// Do something with the data
// ...
use GatoGraphQL\InternalGraphQLServer\GraphQLServer;
/** PHP: Fetch data to render the block on the server (SSR) */
$block = [
'render_callback' => function(array $attributes, string $content): string {
$dryGraphQLQuery = __DIR__ . '/blocks/my-block/graphql-documents/fetch-posts-for-my-block.gql';
$response = GraphQLServer::executeQueryInFile($dryGraphQLQuery);
$responseContent = json_decode($response->getContent(), true);
if (isset($responseContent["errors"]) {
return __('Oops, executing the query produced errors');
}
$data = $responseContent["data"];
// Do something with the data
// $content = $this->useGraphQLData($content, $data);
return $content;
},
];
register_block_type("namespace/my-block", $block);
<script type="module">
import { h, render } from 'https://esm.sh/preact';
renderPost(1);
async function renderPost(postId) {
const response = await fetch( "https://mysite.com/graphql/", {
body: JSON.stringify( {
query: `{ post(by: { id: ${ postId } }) { blockDataItems } }`
} )
} );
const json = await response.json();
if (json.errors) { return; }
const blocks = json.data.post?.blockDataItems;
const App = h('div', {}, blocks.map(mapBlockToComponent));
render(App, document.body);
}
function mapBlockToComponent(block) {
if (block.name === 'core/heading') return Heading(block);
if (block.name === 'core/paragraph') return Paragraph(block);
if (block.name === 'core/image') return Image(block);
return null;
}
function Heading(props) {
return h('h2', { dangerouslySetInnerHTML: { __html: props.attributes.content } });
}
function Paragraph(props) {
return h('p', { dangerouslySetInnerHTML: { __html: props.attributes.content } });
}
function Image(props) {
return h('img', { src: props.attributes.url });
}
</script>
query GetPostAndExportData($postId: ID!) {
post(by: { id : $postId }) {
author {
id @export(as: "authorID")
}
categories {
id @export(as: "categoryIDs", type: LIST)
}
rawContent @export(as: "rawContent")
rawExcerpt @export(as: "excerpt")
featuredImage {
id @export(as: "featuredImageID")
}
tags {
id @export(as: "tagIDs", type: LIST)
}
rawTitle @strAppend(string: " (Copy)") @export(as: "title")
}
}
mutation DuplicatePost @depends(on: "GetPostAndExportData") {
createPost(input: {
authorBy: { id: $authorID },
categoriesBy: { ids: $categoryIDs },
contentAs: { html: $rawContent },
excerpt: $excerpt
featuredImageBy: { id: $featuredImageID },
tagsBy: { ids: $tagIDs },
title: $title
}) {
status
}
}
query ExportConditionalVariables {
me {
roleNames @remove
isAdminUser: _inArray(value: "administrator", array: $__roleNames)
@export(as: "isAdminUser")
}
}
query RetrieveContentForAdminUser($postId: ID!)
@include(if: $isAdminUser)
{
post(by: { id : $postId }) {
originalContent: content @remove
wpAdminEditURL @remove
content: _sprintf(
string: "%s<p><a href=\"%s\">%s</a></p>",
values: [ $__originalContent, $__wpAdminEditURL, "(Admin only) Edit post" ]
)
}
}
query RetrieveContentForNonAdminUser($postId: ID!)
@skip(if: $isAdminUser)
{
post(by: { id : $postId }) {
content
}
}
query ExecuteAll
@depends(on: ["ExportConditionalVariables", "RetrieveContentForAdminUser", "RetrieveContentForNonAdminUser"]
) {
id @remove
}
query TransformAndExportData($replaceFrom: String! $replaceTo: String!, $limit: Int! = 5, $offset: Int! = 0) {
posts: posts( pagination: { limit: $limit, offset: $offset } ) {
title
excerpt
@strReplaceMultiple(
search: $replaceFrom
replaceWith: $replaceTo
affectAdditionalFieldsUnderPos: 1
)
@deferredExport(
as: "postInputs"
type: DICTIONARY
affectAdditionalFieldsUnderPos: 1
)
}
}
mutation UpdatePost($limit: Int! = 5, $offset: Int! = 0)
@depends(on: "TransformAndExportData")
{
adaptedPosts: posts( pagination: { limit: $limit, offset: $offset } ) {
id
postInput: _objectProperty(
object: $postInputs,
by: { key: $__id }
) @remove
update(input: $__postInput) {
status
}
}
}
query ExportData($oldPageSlug: String!, $newPageSlug: String!) {
siteURL: optionValue(name: "siteurl")
oldPageURL: _strAppend(after: $__siteURL, append: $oldPageSlug)
@export(as: "oldPageURL")
newPageURL: _strAppend(after: $__siteURL, append: $newPageSlug)
@export(as: "newPageURL")
}
mutation ReplaceOldWithNewURLInPosts
@depends(on: "ExportData")
{
posts( filter: { search: $oldPageURL } ) {
id
rawContent
adaptedRawContent: _strReplace(
search: $oldPageURL
replaceWith: $newPageURL
in: $__rawContent
)
update(input: {
contentAs: { html: $__adaptedRawContent }
}) {
status
post {
title
excerpt
}
}
}
}
mutation InjectBlock($limit: Int! = 5, $offset: Int! = 0) {
posts: posts( pagination: { limit: $limit, offset: $offset } ) {
rawContent
adaptedRawContent: _strRegexReplace(
in: $__rawContent,
searchRegex: "#(<!-- /wp:paragraph -->[\\s\\S]+<!-- /wp:paragraph -->[\\s\\S]+<!-- /wp:paragraph -->)#U",
replaceWith: "$1<!-- mycompany:black-friday-campaign-video -->\n<figure class=\"wp-block-video\"><video controls src=\"https://mysite.com/videos/BlackFriday2023.mp4\"></video></figure>\n<!-- /mycompany:black-friday-campaign-video -->",
limit: 1
)
update(input: { contentAs: { html: $__adaptedRawContent } }) {
status
}
}
}
mutation RemoveBlock {
posts(filter: { search: "\"<!-- /mycompany:black-friday-campaign-video -->\"" } ) {
rawContent
adaptedRawContent: _strRegexReplace(
in: $__rawContent,
searchRegex: "#(<!-- mycompany:black-friday-campaign-video -->[\\s\\S]+<!-- /mycompany:black-friday-campaign-video -->)#",
replaceWith: ""
)
update(input: { contentAs: { html: $__adaptedRawContent } }) {
status
}
}
}
query GetImageBlockImageURLs($postID: ID!) {
post(by: { id: $postID } ) {
coreImageURLs: blockFlattenedDataItems(
filterBy: { include: "core/image" }
)
@underEachArrayItem(
passValueOnwardsAs: "blockDataItem"
)
@applyField(
name: "_objectProperty"
arguments: {
object: $blockDataItem,
by: {
path: "attributes.url"
}
}
setResultInResponse: true
)
@arrayUnique
}
}
use \GatoGraphQL\InternalGraphQLServer\GraphQLServer;
add_action(
'wp_insert_post',
function (int $postID, WP_Post $post) {
// Provide the query to execute
$query = '{ ... }';
$variables = [
'postTitle' => $post->post_title,
'postContent' => $post->post_content,
'postURL' => get_permalink($post->ID),
]
GraphQLServer::executeQuery($query, $variables, 'SendEmail');
},
10,
2
);
wp_schedule_event(
time(),
'daily',
'gatographql__execute_persisted_query',
[
'daily-stats-by-email-number-of-comments',
[ 'to' => ['admin@mysite.com'] ],
'SendDailyStatsByEmailNumberOfComments',
1 // This is the admin user's ID
]
);
query GetDataFromMailchimp {
mailchimpListMembersJSONObject: _sendJSONObjectItemHTTPRequest(input: {
url: "https://us7.api.mailchimp.com/3.0/lists/{LIST_ID}/members",
method: GET,
options: {
auth: {
username: "{ USER }",
password: "{ API_TOKEN }"
}
}
})
@underJSONObjectProperty(by: { key: "members"})
@underEachArrayItem
@underJSONObjectProperty(by: { key: "email_address"})
@export(as: "mailchimpListMemberEmails")
}
query GetUsersUsingMailchimpSubscriberEmails
@depends(on: "GetDataFromMailchimp")
{
users(filter: { searchBy: { emails: $mailchimpListMemberEmails } } ) {
id
name
email
}
}
query {
usersWithWebsiteURL: _sendJSONObjectCollectionHTTPRequest(
input: {
url: "https://some-wp-rest-api.com/wp-json/wp/v2/users/?_fields=id,name,url"
}
)
@underEachArrayItem(
passValueOnwardsAs: "userDataEntry"
affectDirectivesUnderPos: [1, 2, 3]
)
@applyField(
name: "_objectProperty"
arguments: {
object: $userDataEntry
by: {
key: "url"
}
}
passOnwardsAs: "websiteURL"
)
@applyField(
name: "_isEmpty"
arguments: {
value: $websiteURL
}
passOnwardsAs: "isWebsiteURLEmpty"
)
@if(condition: $isWebsiteURLEmpty)
@setNull
@arrayFilter
}
Your WordPress site, at the center of the web
Execute HTTP requests against any webserver. Fetch data from external APIs, interact with cloud-based services, send pings when a comment is added, and much more.
Augment your capabilities via powerful extensions
🐾
Access Control
Grant granular access to your users, down to the operation, field and directive level
🐾
Cache Control
Cache the GraphQL response on the client and CDN via standard HTTP caching
🐾
Multiple Query Execution
Combine multiple queries and execute them together, reusing their state and data
🐾
Automation
Automate admin tasks, executing queries when some event happens, and scheduling them via WP-Cron
🐾
Internal GraphQL Server
Install an internal GraphQL Server, that can be invoked within your application, using PHP code
🐾
PHP Functions
Add fields and directives which expose functionalities commonly found in programming languages
Handle large data sets with ease
Update thousands of resources with granular control, in a single operation. Create the queries in advance, and let other people execute them (even your clients).



Translate your blog post using Google Translate, attribute by attribute within the blocks
Retrieve Gutenberg block data, modify it as needed, and store it again in the DB. Translate the text attributes in the post's blocks, and keep editing the translated post right in the editor.
Distribute content across a network of sites
Have an origin WordPress server hosting the single source of truth for all your content, and distribute it to downstream WordPress sites.
Plugin screenshots
Private GraphiQL client
Execute your GraphQL queries in the wp-admin via a private GraphiQL client.
Private Interactive Schema client
Interactively browse the private GraphQL schema in the wp-admin, exploring all connections among entities.
Public GraphiQL client
The GraphiQL client for the single endpoint is exposed to the Internet.
Public Interactive Schema client
Interactively browse the public GraphQL schema exposed for the single endpoint.
Persisted Queries
Persisted queries are pre-defined and stored in the server.
Executing Persisted Queries
Requesting a persisted query URL will retrieve its pre-defined GraphQL response.
Custom Endpoints
We can create multiple custom endpoints, each for a different target.
Schema Configurations
All endpoints (the single endpoint, custom endpoints, persisted queries, and internal endpoints for the application) are configured via Schema Configurations.
Schema Configurations
We can create many Schema Configurations, customizing them for different users or applications.
Public or Private Endpoints
Custom endpoints and persisted queries can be public, private and password-protected.
Endpoint Categories
Manage custom endpoints and persisted queries by adding categories to them.
Security
We can configure exactly what custom post types, options and meta keys can be queried on an endpoint by endpoint basis.
Settings
Configure every aspect from the plugin via the Settings page.
Modules
Modules providing different functionalities and GraphQL schema extensions can be enabled and disabled.
Extensions
Augment the plugin functionality and GraphQL schema via extensions.
Recipes
The Recipes section contains example queries ready to copy/paste and use.
Access Control (extension)
Validate who can access the endpoint in granular fashion (down to the operation, field and directive level), based on user roles and capabilities, visitor IP, and more.
Cache Control (extension)
Cache the API response via standard HTTP caching, with the max-age automatically calculated based on the fields present in the query.
Field Deprecation via UI (extension)
Deprecate fields to evolve the GraphQL schema directly from the user interface.