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
Handle all your admin tasks and content workflows with Gato GraphQL
Interact with external services
Automatically send your new post to Twitter, retrieve your newsletter list from Mailchimp, fetch a document from Google Drive, send a ping to an external service when a comment is added, and much more.
Automate your tasks
Automatically add a mandatory block to a post when it's created, send a notification when a comment is added, send a daily summary of activity by email, trigger actions when some custom condition happens, and much more.
Distribute content across a network
Have an origin WordPress server hosting the single source of truth for all your content, and distribute it to downstream WordPress sites.

Handle large data sets with ease
Update thousands of resources with granular control, in a single operation. Create the queries in advance, and let your clients execute them.

And much more!
Gato GraphQL is a versatile tool to interact with data.
Input your GraphQL query and deal with your task.
- ✅ Translate your blog posts using Google Translate
- ✅ Regex search/replace content
- ✅ Adapt the output from an API as input to another one
- ✅ Restrict access to your public API
- ✅ Cache your API response without additional libraries
Access the full power of GraphQL at your most convenient
Compose your queries directly in the wp-admin, store them in the database, and trigger them manually, via code, hooks or WP-Cron.
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!"
}
}
}
}
Use the combined power of 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

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
Browse the ever-growing catalogue of recipes and customize them to your 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
}
Power your headless application
Use Gato GraphQL to fetch data from WordPress on the server-side, render the page using your framework of choice in the client.

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 } ] } }
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.
Subscribe to our newsletter
Receive updates on new releases, extensions, recipes, and more.