Introduction to GraphQL
This guide explains how to read and build Catalog GraphQL operations using the generated API reference. By the end you should be able to:
- Read a GraphQL query or mutation and its Variables block together.
- Match each
$variablein an operation to a key in the Variables JSON. - Tell a query from a mutation, and know when each is required.
- Build a request that reads data (
getTables), one that creates data (createColumns), and one that removes data (removeTeamUsers). - Connect a response JSON object back to the fields you selected.
- Find argument and field details for any operation in the Types reference.
What Is GraphQL?
GraphQL is a text format for asking an API for data, or for asking it to make a change. You write a short operation, list the fields you want back, and the server returns JSON shaped like your request. For background outside Catalog, see the GraphQL.org introduction.
Query Versus Mutation
Every Catalog operation starts with one of two keywords:
| Keyword | Use in Catalog | Example |
|---|---|---|
query | Read metadata. Nothing changes. | query GetTables |
mutation | Create, update, or remove metadata. | mutation CreateColumns, mutation RemoveTeamUsers |
This guide walks through one of each: getTables to read, createColumns to create, and removeTeamUsers to remove. The three together cover the shapes you'll see across the rest of the reference.
Reading a Query: Get Tables
The Get tables operation lists tables in Catalog. It's a query, so it only reads data, and nothing in your Catalog instance changes when you run it.
The Operation Signature
Every operation page shows a signature like this one for getTables:
getTables(
pagination: PublicPagination
sorting: [TableSorting!]
scope: GetTablesScope
): GetTablesOutput!
Read this the way you'd read a function definition:
| Piece | What it means |
|---|---|
getTables | The operation name. Spell it exactly as documented. It also identifies the operation when you send the request over HTTP (see Send the Request with cURL below). |
(pagination: ..., sorting: ..., scope: ...) | The arguments this operation accepts, with their types. All three are optional here, since none has a trailing !. |
: GetTablesOutput! | The shape of the data you get back. The ! means getTables always returns a GetTablesOutput object, never null. |
Writing the Query
To actually call getTables, wrap it in a named query operation and declare the variables you intend to use:
query GetTables($pagination: PublicPagination) {
getTables(pagination: $pagination) {
totalCount
data {
id
name
}
}
}
Breaking down line 1:
| Piece | Name | What it means |
|---|---|---|
query | Operation type | This request only reads data. |
GetTables | Operation name | A label you choose. Naming it after the root field (getTables) keeps things easy to trace, but it isn't required to match. |
($pagination: PublicPagination) | Variable declaration | This operation accepts one variable, $pagination, typed as PublicPagination. The $ marks it as a placeholder filled in from Variables JSON. |
Line 2 is the root field, the actual operation you're calling, matched up with the argument you declared:
| Piece | What it means |
|---|---|
getTables | Must match the operation name in the schema exactly. |
(pagination: $pagination) | Passes your declared variable into the operation's pagination argument. The left-hand pagination is the argument name from the signature; the right-hand $pagination is your variable. |
Everything inside the outer { } is the selection set: the fields you want back.
totalCountreturns the total number of matching tables across all pages.data { id name }returns the table records on this page, with onlyidandnameincluded on each row.
GraphQL only returns fields you ask for. Add description inside data { } and it appears in the response when present; leave it out and it doesn't, even though Catalog stores it. Each row under data is a Table. See that type's page for other optional fields such as slug or schema.
Returning More Fields
The selection set isn't fixed. It's the main way you control the size and shape of the response. Adding a field to the query adds it to the response; nothing else changes.
Start with the minimal version from above, asking for only id and name:
query GetTables($pagination: PublicPagination) {
getTables(pagination: $pagination) {
data {
id
name
}
}
}
{
"data": {
"getTables": {
"data": [
{ "id": "abc123", "name": "FCT_ORDERS" },
{ "id": "def456", "name": "DIM_CUSTOMERS" }
]
}
}
}
Each table object in the response has exactly two keys, id and name, because that's all the selection set asked for. totalCount isn't in the response either, for the same reason: it was never requested at the top level.
Now add totalCount back at the top level, and description and slug inside data { }:
query GetTables($pagination: PublicPagination) {
getTables(pagination: $pagination) {
totalCount
data {
id
name
description
slug
}
}
}
{
"data": {
"getTables": {
"totalCount": 42,
"data": [
{
"id": "abc123",
"name": "FCT_ORDERS",
"description": "Daily order fact table",
"slug": "fct-orders"
},
{
"id": "def456",
"name": "DIM_CUSTOMERS",
"description": null,
"slug": "dim-customers"
}
]
}
}
}
Two things changed in the response, and both trace directly back to the query:
totalCountnow appears once, at the same level asdata, because it was added to the selection set at that level.- Every object inside
datanow has four keys instead of two.descriptionandslugwere added once in the selection set, and Catalog applies that to every row in the list. You don't repeat field names per row.
description came back as null for DIM_CUSTOMERS. This is normal: you asked for the field, and Catalog returned it, but no description has been set on that table. A field appearing as null and a field being absent mean different things. null means you asked for the field but nothing is set. Absent means you never asked for it. Required fields marked with ! on the Table type cannot return null; optional fields can.
The same rule applies anywhere you see { } in this guide, including the nested schema { database { name } } relation further down. Adding a field anywhere inside a selection set adds it to the response at that exact position, and nowhere else.
Matching Variables to the Query
The query declares $pagination. The Variables JSON supplies its value under the matching key, "pagination":
{
"pagination": {
"nbPerPage": 10,
"page": 0
}
}
| In the query | In Variables JSON |
|---|---|
$pagination: PublicPagination | not used as a key. This just declares the variable and its type |
getTables(pagination: $pagination) | "pagination": { ... }, the value passed to the argument |
PublicPagination, a schema type | never a JSON key. It describes which keys are allowed inside the value, here nbPerPage and page |
PublicPagination is an input type: it tells you what's allowed inside the object, not where to put the object. nbPerPage is capped at 500; page is zero-based.
Variables JSON is plain JSON, not GraphQL. Don't prefix its keys with $. Copy the block from the operation's Example as a starting point, then edit the values.
Reading the Response
{
"data": {
"getTables": {
"totalCount": 42,
"data": [
{ "id": "abc123", "name": "FCT_ORDERS" },
{ "id": "def456", "name": "DIM_CUSTOMERS" }
]
}
}
}
The response nests under data.getTables, the same root field name you called, with totalCount and data filled in because you asked for them. Nothing else appears.
Adding Filters, Sorting, and Relations
getTables also accepts sorting and scope, and lets you pull in related objects:
query GetTables(
$pagination: PublicPagination
$sorting: [TableSorting!]
$scope: GetTablesScope
) {
getTables(pagination: $pagination, sorting: $sorting, scope: $scope) {
totalCount
data {
id
name
schema {
name
database {
name
}
}
}
}
}
{
"pagination": { "nbPerPage": 20, "page": 0 },
"sorting": [{ "sortingKey": "name", "direction": "ASC" }],
"scope": { "nameContains": "orders" }
}
| Argument | Purpose |
|---|---|
pagination | Page size (nbPerPage, max 500) and zero-based page. |
sorting | A list of { sortingKey, direction } objects. Note the [TableSorting!] type, meaning a JSON array of sorting objects. |
scope | Filters, combined with AND logic. |
schema { name database { name } } is a nested relation. Check the Response section on the operation page to see whether paths like schema.database are available on that endpoint. Deeper trees mean larger responses, so add nesting only when your integration needs the linked metadata.
Writing a Mutation: Create Columns
Queries read. Mutations write. Create columns creates one or more columns, so it's a good next example for seeing how a mutation differs from a query. Since it works on a list of inputs at once, it also shows how list types flow through both the request and the response.
The Operation Signature
createColumns(
data: [CreateColumnInput!]!
): [Column!]!
Compare this to getTables. The overall shape is the same: a name, arguments, and a return type. A few details are new:
- The argument is named
data, and it's a list type:[CreateColumnInput!]!. Read this from the inside out.CreateColumnInput!means each item in the list is required with nonullentries. The outer!means the list itself is required: you can't omitdataor passnull, though an empty list[]is still a list. - The return type,
[Column!]!, follows the same pattern: a required list of requiredColumnobjects, one per column created. createColumnsallows no nested relations in its response. Whatever fields you select onColumn, you get back flat.
This is a useful contrast with getTables, where the list lived inside an output object (GetTablesOutput.data). Here, the operation's return type is the list directly.
Writing the Mutation
mutation CreateColumns($data: [CreateColumnInput!]!) {
createColumns(data: $data) {
id
name
}
}
Reading line 1 against the getTables example: the keyword changed from query to mutation, but the pattern is identical: name, then ($variable: Type). The ! on [CreateColumnInput!]! means the Variables JSON must include "data" as an array. Omitting it, or sending null, is a validation error rather than an empty result.
Variables
{
"data": [
{
"tableId": "abc123",
"name": "customer_email",
"dataType": "STRING",
"externalId": "customer_email",
"isNullable": true
},
{
"tableId": "abc123",
"name": "customer_phone",
"dataType": "STRING",
"externalId": "customer_phone",
"isNullable": true
}
]
}
| In the mutation | In Variables JSON |
|---|---|
$data: [CreateColumnInput!]! | declares the required variable and its list type |
createColumns(data: $data) | "data": [ ... ], a JSON array matching the [ ] in the type |
CreateColumnInput | describes which keys each item in the array may contain. Open the Types reference for the full field list |
The JSON array here is the direct counterpart of the [ ] in [CreateColumnInput!]!. Whenever you see square brackets in a type, send a JSON array in the matching spot.
Reading the Response
{
"data": {
"createColumns": [
{ "id": "col_001", "name": "customer_email" },
{ "id": "col_002", "name": "customer_phone" }
]
}
}
The response nests under data.createColumns, same as every other operation. Because the return type is [Column!]! rather than a single object, the value is a JSON array with one entry per input item, in the same order you sent them: two columns in, two columns back.
Writing a Mutation: Remove Team Users
Remove team users removes users from a Team. It's a good second mutation example because its return type is different from both prior examples: it returns a plain Boolean, not an object.
The Operation Signature
removeTeamUsers(
data: TeamUsersInput!
): Boolean!
Same data: SomeInput! pattern seen on createColumns. The difference worth noticing is the return type: Boolean! instead of an object or list type. That changes what you're allowed to select in the response.
Writing the Mutation
mutation RemoveTeamUsers($data: TeamUsersInput!) {
removeTeamUsers(data: $data)
}
Notice there's no { } selection set after removeTeamUsers(data: $data). Selection sets are only valid on fields that return an object or list. Scalars like Boolean, String, Int, and ID are leaf values, so you select the field itself and stop. Adding { } here would be a syntax error.
Variables
{
"data": {
"id": "team_123",
"emails": ["user456@example.com", "user789@example.com"]
}
}
emails as an array matches the [String!]! type on TeamUsersInput. Whenever a Variables value is a JSON array, expect a [Type] or [Type!] in the corresponding input type.
Reading the Response
{
"data": {
"removeTeamUsers": true
}
}
No nested object, no selected fields: just the scalar result. A true means the users were removed. This is the simplest response shape in the API, and a useful contrast to the nested data.getTables and data.createColumns shapes above.
Comparing All Three
getTables | createColumns | removeTeamUsers | |
|---|---|---|---|
| Operation type | query | mutation | mutation |
| Arguments | pagination, sorting, scope: all optional | data: [CreateColumnInput!]!: required list | data: TeamUsersInput!: required |
| Return type | GetTablesOutput!, object with totalCount and data list | [Column!]!, list of objects returned directly | Boolean!, scalar |
| Selection set required? | Yes, must select at least one field | Yes, must select at least one field on Column | No, Boolean is a scalar with no { } |
| Changes data? | No | Yes, creates one or more columns | Yes, removes team members |
The pattern that carries across all three: declare your variables on the operation line with $name: Type, pass them into the root field's arguments, and shape the Variables JSON to match. What differs is whether the argument is optional or required, marked with !, whether it's a single value or a list, marked with [ ], and whether the return type needs a selection set. Objects and lists of objects do; scalars don't.
Types in the Catalog Schema
Every argument and field in the reference has a type. Open the Types section, or click a type name directly from an operation page, when you need a full field list or to check which fields are Required.
Syntax You Will See
| Syntax | Meaning | Example |
|---|---|---|
String | Text | "revenue" |
Int | Integer | 10 |
Boolean | true or false | true |
ID | Identifier string | "abc123" |
SomeInput | Object shape you send in Variables | { "nbPerPage": 10, "page": 0 } |
! | Required | CreateColumnInput!, Boolean! |
[SomeType] | List, sent or received as a JSON array | [{ "sortingKey": "name", "direction": "ASC" }] |
[SomeType!]! | Required list of required items | ["user_456", "user_789"] |
Inputs, Objects, and the Rest
| Kind | Role in Catalog | Seen in this guide as |
|---|---|---|
| Input | Shape of objects you send in Variables | PublicPagination, CreateColumnInput, TeamUsersInput |
| Object | Shape of data you can select in { } | GetTablesOutput, Table, Column |
| Scalar | A single leaf value with no selection set | Boolean, String, ID, Int |
| Enum | A fixed set of string values | sort direction, for example ASC or DESC |
The same distinction explains why getTables { totalCount data { id name } } needs braces and removeTeamUsers(data: $data) doesn't: the first returns an Object, the second a Scalar.
Using the Reference
Generated pages come from the public GraphQL schema file. When the product schema changes, the reference updates with it. For the three-panel layout, Try it form, code samples, and a step-by-step walkthrough of each reference block, see Using the GraphQL Reference.
Send the Request with cURL
Everything above describes the GraphQL text and the Variables JSON. To call the API from a terminal or script, send both inside an HTTP POST body. See Catalog GraphQL API for the ?op= query parameter and regional hosts.
curl -X POST 'https://api.castordoc.com/public/graphql?op=getTables' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Token YOUR_CATALOG_API_TOKEN' \
-d '{
"query": "query GetTables($pagination: PublicPagination) { getTables(pagination: $pagination) { totalCount data { id name } } }",
"variables": {
"pagination": { "nbPerPage": 10, "page": 0 }
}
}'
The same pattern applies to mutations. Swap the ?op= value, the query string, and the variables object:
curl -X POST 'https://api.castordoc.com/public/graphql?op=removeTeamUsers' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Token YOUR_CATALOG_API_TOKEN' \
-d '{
"query": "mutation RemoveTeamUsers($data: TeamUsersInput!) { removeTeamUsers(data: $data) }",
"variables": {
"data": { "id": "team_123", "emails": ["user456@example.com", "user789@example.com"] }
}
}'
Use a token from Getting Your Catalog API Keys. For US-hosted Catalog instances, replace the host with api.us.castordoc.com.