Headless API
Contents
Enonic is a hybrid CMS, which means that it can be used to manage and deliver content to your frontend.
Now that we’ve created some content, let’s see how we can access and use it in via API - headless style.
In order to do that we’ll introduce Guillotine, the app that’s responsible for automatically generating GraphQL APIs for your content.
If you are not interested in using Enonic in headless mode, you can skip to the next chapter, in which the standard way of using XP will be introduced. |
Installing Guillotine
The first thing you need to do in order to get going is installing the Guillotine application. It is available on Enonic Market.
Fortunately, you can install Guillotine directly from your Enonic XP instance’s admin console.
-
From the XP menu (top right), choose
Applications
. -
Click Install from the top menu
-
Search for
Guillotine
, and click Install`.
Query playground
Back in Content Studio, you should now have access to a new item called Query playground
in the left hand menu. Open it, and you should see this:

Query playground is an interactive GraphQL API explorer. You enter your query in the text area to your left and when you execute it, you’ll get the results of the query on the other side of the screen.
GraphQL is a graph "query language". It allows you to create highly specific queries for exactly the content that you need (visit the official GraphQL docs to learn more).
As an example on how to retrieve some minimal info about your previously created artists, run the following query:
{
guillotine {
getChildren(key:"/artists") {
displayName
_path
_id
}
}
}
After running, you should get something like this on the right area:

Before trying to come up with complex queries, let’s make familiarize ourselves with Query Playground.
Basic buttons

-
play button, will execute your query. Shortcut is ctrl+enter
-
prettify button, will properly format your query, removing comments and unecessary spaces / indentation. Shortcut is ctl+shift+p
-
merge fragments button, will rewrite your query based on the declared fragments. Shortcut is ctl+shift+m
-
copy button, will copy your query to your clipboard. Shortcut is ctl+shift+c
-
docs button, will open a section showing all details about the schemas you have avaiable to use and query on
-
history button, will open a section showing all previously executed queries
-
tab button, will create new tabs for you to manage testing multiple queries
-
draft / master buttons will switch between those two repositories for your queries to run on
-
re-fetch schema button, will update your graphql schemas based on recent changes
-
shortkeys button, will open a dialog displaying all short keys that can be used to execute actions
-
settings button, will open settings dialog
Schemas
Now, locate and click on the docs button. On this opened section you have detailed info about all schemas available on your project.
On the top search bar in the docs section, search for artist
and locate the com_example_myproject_Artist
schema:

Note that it’s said that the Artist
schema implements the Content
schema. This basically means that the Artist
schema will have the structure defined by the Content
schema, together with possibly some additional structure… in this case, that additional structure is a data
field, defined by com_example_myproject_Artist_Data
schema:

Note that this matches the structure defined by the Artist content type, which is a natural thing to expect. |
Querying with inline fragments
We’ve used the Docs section to inspect the structure of the Artist
schema. Let’s now, based on a broader understanding of this schema, we’ll query to get proper data from artists.
Close the docs section, head back to the text area to edit your query and remove displayName
, _path
and _id
from the query we introduced earlier.
Now, with your cursor on the inner curly braces of the query, press ctrl+space to get a list of suggestions about what fields you can request on your query:

As can be seen, we do not have the artist name
and about
fields available, and the reason for that is because the getChildren
field can return any content that implements the Content
schema, but it can’t directly infer what schema that is, which in this case is the com_example_myproject_Artist
schema:

So to proceed, we use inline fragments:
{
guillotine {
getChildren(key: "/artists") {
... on com_example_myproject_Artist {
_id
data {
name
about
}
}
}
}
}
With this, we basically built a query that understands that under "/artists"
path we have content types of Artist
, and based on this we can request general content data, such as _id
, but also specific content data, such as the artist name and about:

Query with dataAsJson
If you don’t need or care about types when creating your queries, you can ignore the previously presented inline fragment approach and use the field dataAsJson
:
{
guillotine {
getChildren(key: "/artists") {
_id
dataAsJson
}
}
}

Advanced queries
We’ll now introduce and show examples of usaged of three different fields underneath guillotine
field, that allos us to search the system for specific data.
Query
There is a query
field underneath guillotine
which allows you to search the system for what you want. This field accepts a number of optional, named arguments to help you find exactly what you’re looking for. These arguments are:
-
contentTypes
-
The content types we’re looking for. Can either be a lone string if you’re looking for a single content type, or a list if you want to search among multiple types.
-
filters
-
A list of filters to apply to the results after the query has been executed. We won’t get into them here, so if you want to learn more about them, head to the filters documentation.
-
first
-
Specify how many nodes you want at most in your result. If your query yields a list of results, you can specify that you only want the first
n
results, wheren
is a positive integer. Works well in concert with offset. -
offset
-
Specify how many results you want to cut from the beginning of the list of results. Most commonly used for pagination.
-
query
-
A NoQL query string.
-
sort
-
A NoQL sort string.
For instance, using the data set we’ve created previously in this guide, we could run a query like this:
{
guillotine {
query(
first: 1
contentTypes: "com.example.myproject:artist" (1)
sort: "displayName desc"
) {
_id
displayName
}
}
}
1 | To query for multiple types, enclose them in square brackets and separate them with commas. |
Note now that this query is getting the last artist that is alphabetically sorted by name, regardless of where the content type is located in the tree structure.
To perform a more advanced query, you can use the query
argument.
The following GraphQL query uses the n-gram query functionality to find all the artists that have a term (part of a text) that starts with rap
somewhere in their data.
{
guillotine {
query(
contentTypes: "com.example.myproject:artist"
query: "ngram('_allText', 'rap')"
sort: "displayName asc"
) {
displayName
}
}
}
Naturally, you can also use the standard boolean operators to combine or negate clauses. If you wanted to find all artists that mention both rapping and singing, you could try a query like this:
{
guillotine {
query(
contentTypes: "com.example.myproject:artist"
query: "ngram('_allText', 'rap') AND ngram('_allText', 'sing')"
sort: "displayName asc"
) {
displayName
}
}
}
To learn more about query capabilities, visit the NoQl reference |
QueryDSL
Just like there is a query
field underneath guillotine
, the API also provides other fields such as queryConnection
, queryDsl
and queryDslConnection
.
We’ll not go into much details, but basically, the fields with connection in their name have new parameters for aggregating data, whereas in the fields with dsl (doman specific language), the query argument is not a string but Guillotine own specific DSL.
To make it clear, we’ll rewrite the last query that used ngram
using the queryDsl field:
{
guillotine {
queryDsl(
query: {
boolean: {
must: {
term: {
field: "type"
value: {
string: "com.example.myproject:artist"
}
}
ngram: {
fields: "data.about"
query: "rap sing"
operator: AND
}
}
}
}
sort: {
field: "displayName"
direction: ASC
}
) {
displayName
}
}
}
As expected, the results from this and the previous query must match, since we’re essentially doing the same, but with queryDsl
it is unlikely we’ll create invalid queries, since we are using a fixed structure rather than a string.
If you’ve previously used Elasticsearch API to query data, you’ll be quite familiar with how queryDsl works! |
To learn more about queryDSL capabilities, visit the DSL reference |
QueryDslConnection
To finish this chapter, we’ll add a new birthyear field to the artist content type, update all artist to have data on this field, and then use queryDslConnection
field to get aggregated data. Let’s get to work!
-
Start by updating the Artist content type to the following structure:
<content-type>
<display-name>Artist</display-name>
<description>Information about an artist</description>
<super-type>base:structured</super-type>
<form>
<input name="name" type="TextLine">
<label>Name</label>
<help-text>
The artist's name (if different from their professional moniker).
</help-text>
</input>
<input type="TextArea" name="about">
<label>About the artist</label>
</input>
<input type="Date" name="birthday" >
<label>Birthday</label>
</input>
</form>
</content-type>
-
Run
enonic project deploy
in the terminal on the root directory of the project -
Update each of the previously created artists with the following dates:
-
P!nk: 1979-09-08
-
Missy Elliot: 1971-07-01
-
Cardi B.: 1992-10-11
-
-
Go back to Query playground and click on the re-fetch schema button. This will update the Artist data schema to contain the birthday structure, which is in sync with the Artist content type structure
-
Run the following query on the playground:
{ guillotine { queryDslConnection( query: { boolean: { must: { term: { field: "type" value: { string: "com.example.myproject:artist" } } exists: { field: "data.birthday" } } } } aggregations: { name: "generationsAggregation" dateRange: { field: "data.birthday" ranges: [ { key: "The Greatest Generation" from: "1901" to: "1927" } { key: "The Silent Generation" from: "1928" to: "1945" } { key: "The Baby Boomer Generation" from: "1946" to: "1964" } { key: "Generation X" from: "1965" to: "1980" } { key: "Millennials" from: "1981" to: "1996" } { key: "Generation Z" from: "1997" to: "2012" } { key: "Gen Alpha" from: "2013" to: "2025" } ] } } ) { aggregationsAsJson } } }
And this are the results
{
"data": {
"guillotine": {
"queryDslConnection": {
"aggregationsAsJson": {
"generationsAggregation": {
"buckets": [
{
"key": "The Greatest Generation",
"docCount": 0,
"from": "1901-01-01T00:00:00Z",
"to": "1927-01-01T00:00:00Z"
},
{
"key": "The Silent Generation",
"docCount": 0,
"from": "1928-01-01T00:00:00Z",
"to": "1945-01-01T00:00:00Z"
},
{
"key": "The Baby Boomer Generation",
"docCount": 0,
"from": "1946-01-01T00:00:00Z",
"to": "1964-01-01T00:00:00Z"
},
{
"key": "Generation X",
"docCount": 2,
"from": "1965-01-01T00:00:00Z",
"to": "1980-01-01T00:00:00Z"
},
{
"key": "Millennials",
"docCount": 1,
"from": "1981-01-01T00:00:00Z",
"to": "1996-01-01T00:00:00Z"
},
{
"key": "Generation Z",
"docCount": 0,
"from": "1997-01-01T00:00:00Z",
"to": "2012-01-01T00:00:00Z"
},
{
"key": "Gen Alpha",
"docCount": 0,
"from": "2013-01-01T00:00:00Z",
"to": "2025-01-01T00:00:00Z"
}
]
}
}
}
}
}
}
Which shows that two of those artists are from Generation X, and one of them is a Millenial.
Dive deeper
-
For more query examples and information about how to get up and running with Guillotine, you can check out the headless CMS intro guide or the Guillotine docs
-
If you want to understand how to extend available schemas on the GraphQL Api, you can head to Extending section on Guillotine docs or check some examples on this guide
-
If you want to learn more about GraphQL in general, the Introduction to GraphQL documentation is a good place to start