- Introducing VizQL Data Service (again)
- Why Would I Use VDS?
- Before You Dive In
- Know Before You Go (Querying Datasource Metadata)
- The Anatomy of a VizQL Data Service Query
- Building Your First VDS Queries
- Adding Custom Fields/Calculations
- Applying Filters to Your Queries
- Additional Options
- Bonus Gifts 🎁
- Additional Videos
- This Is Where I Leave You…
Introducing VizQL Data Service (again)
You might be thinking, “Hey Kyle, didn’t you already write a post about VDS?!” – and you’re correct!
When I first wrote about this topic last August, the API was still in beta and was slowly being rolled out only to portions of Tableau Cloud. As of the time of writing, the VizQL Data Service is fully rolled out on Cloud and will be available for Tableau Server when you upgrade to 2025.x – so it’s a perfect time to take a deeper look!
Why Would I Use VDS?

Here’s the TL;DR of my post from last summer, which I think sums it up well:
The VizQL Data Service allows programmatic access to published Datasources on Tableau Cloud and Server. Using API calls, users can query data directly from Cloud/Server in development-friendly JSON! These queries can contain existing columns, custom calculations, and filters. This ‘headless BI’ experience means you can ask for exactly what you need from your data – while still enjoying the full benefits of a Tableau published Datasource, such as group permissions, row-level security, etc.
Fans of Embedded Analytics for Tableau, Python/R, and other data science applications will immediately appreciate the ability to just get the data you already know in a form you can take anywhere – your imagination is the only limit!
Before You Dive In
A few quick things to know before writing your first VDS query:
Setup/Permissions:
Great news! The VizQL Data Service requires no additional setup or licensing; however, there is a permission that must be enabled for the datasource(s) you want to query:
Authentication
Last year, I did another deep dive post on Tableau API Authentication. Start here to learn about your options for authenticating – they are applicable to VDS, as well!
If you’re already familiar with authenticating with the Tableau REST API, you’re in luck! VDS uses the exact same ‘X-Tableau-Auth’ header, where the value is an API token you received after a sign-in request.
As I’m writing this, there’s no official support for VDS within the tableauserverclient (TSC) library for Python, but who knows – maybe some DataDevs will work together in the near future to change that! 😉
Know Before You Go (Querying Datasource Metadata)

Unless you’ve somehow managed to completely memorize all the columns and their types (weirdo), you probably need a way to get this metadata about your Datasource. Luckily, we can make a quick API request to accomplish this:
Get Metadata w/ Postman
Requirements: Postman | Reference: VizQL Data Service Documentation |
Postman is a great tool for getting started with just about any API! It allows you to make REST API calls and examine their responses without writing any code!
METHOD | POST |
URL | https://YOUR-HOST-ROOT/api/v1/vizql-data-service/read-metadata e.g. https://10ax.online.tableau.com/api/v1/vizql-data-service/read-metadata |
Required Headers: | content-type: application/json, X-Tableau-Auth: YOUR-API-TOKEN-FROM-AUTH-CALL |
Body (JSON) | { |
Response (JSON) | { … ] }
|
NOTES: | • Unlike calls to the REST API, the request URL does not include the site name/ID. This is automatically derived from your API token |
Get Metadata with Python requests
# REQUEST import requests url = r"https://YOUR-HOST-ROOT/api/v1/vizql-data-service/read-metadata" api_token = "YOUR-API-TOKEN" headers = { "content-type": "application/json", "X-Tableau-Auth": api_token, } ds_luid = "YOUR-DS-LUID" req_body = {"datasource": {"datasourceLuid": ds_luid}} resp = requests.post( url, headers=headers, json=req_body, ) print(resp.json())
// RESPONSE { "data": [ { "fieldName": "Calculation_1368249927221915648", "fieldCaption": "Profit Ratio", "dataType": "REAL", "defaultAggregation": "AGG", "logicalTableId": "" }, { "fieldName": "Category", "fieldCaption": "Category", "dataType": "STRING", "defaultAggregation": "COUNT", "logicalTableId": "Orders_ECFCA1FB690A41FE803BC071773BA862" }, { "fieldName": "City", "fieldCaption": "City", "dataType": "STRING", "defaultAggregation": "COUNT", "logicalTableId": "Orders_ECFCA1FB690A41FE803BC071773BA862" }, // ... ] }
NOTE: From this point forward, I’ll just provide the JSON request bodies and responses. You can use the Python and Postman examples above — just replace the ‘req_body’ as you go. There is also a link to a Postman collection later in this post 😉
The Anatomy of a VizQL Data Service Query

Now that we know the fields we have to choose from, we’re ready to start writing a VDS query! I’ll cover most of the basics here, but be sure to bookmark the official docs to keep updated on new features and more advanced use cases.
An outline of a VDS query:
- datasource: This is where you’ll provide the Datasource luid. Here are some tips on different ways to find it!
- options: setting “debug: true” here will provide more verbose error messages. True is the default value, and it can be omitted.
- query: This section will be the meat and bones of your query to VDS. This is where you’ll list the fields you want in response and aggregations to apply. You’ll also define any custom calculations and filters you want to apply here.
The first two bits are pretty self-explanatory, so let’s dive right into building the query!
Building Your First VDS Queries

FYI: I’ll be using everyone’s favorite Datasource for all of these examples – Superstore!
Let’s start extra simple. Here’s a query that will give us the following:
- Order ID
- Customer Name
- Date Added
// REQUEST { "datasource": { "datasourceLuid": "{{datasource_luid}}" }, "options": { "debug": true }, "query": { "fields": [ { "fieldCaption": "Order ID" }, { "fieldCaption": "Customer Name" }, { "fieldCaption": "Order Date", "fieldAlias": "Date Ordered" } ] } }
// RESPONSE { "data": [ { "Order ID": "CA-2020-115525", "Customer Name": "Rick Reed", "Date Ordered": "2020-07-25T00:00:00" }, { "Order ID": "CA-2021-147753", "Customer Name": "Pete Kriz", "Date Ordered": "2021-03-05T00:00:00" }, ... // MANY MORE RESULTS ] }
Things to note in the query and results here:
- fieldCaption: This is the name of the field as it appears in Tableau Desktop or in Web Authoring
- fieldAlias (optional): Allows you to provide an alias for the field in the JSON results – great for normalizing/making them web-safe and easier to work with in code
- This query has no aggregation at all and Order ID is very granular. Thus, the response contains an array of objects representing every row – we’ll see how level of granularity affects response size soon
Now let’s try some aggregation. Let’s query:
- Region
- SUM([Sales])
// REQUEST { "datasource": { "datasourceLuid": "{{datasource_luid}}" }, "options": { "debug": true }, "query": { "fields": [ { "fieldCaption": "Region" }, { "fieldCaption": "Sales", "function": "SUM" } ] } }
When I add a measure to the query, I can then add a key for ‘function’, i.e. aggregation. In this Datasource, Sales is a number, so I want the SUM. Tableau/VDS takes care of the grouping based on the level of granularity implied by my field definitions. Therefore, it’s very important to understand how level of granularity and aggregation works in Tableau. This article from The Data School is a great reference!
Here’s the response:
// RESPONSE { "data": [ { "Region": "West", "SUM(Sales)": 725457.8245000019 }, { "Region": "East", "SUM(Sales)": 678781.239999999 }, { "Region": "Central", "SUM(Sales)": 501239.89080000145 }, { "Region": "South", "SUM(Sales)": 391721.9050000003 } ] } // FOUR TOTAL RESULTS
Now, watch what happens when I add State to this query:
// REQUEST { "datasource": { "datasourceLuid": "{{datasource_luid}}" }, "options": { "debug": true }, "query": { "fields": [ { "fieldCaption": "Region", "sortPriority": 1 }, { "fieldCaption": "State", "sortPriority": 2, "sortDirection": "DESC" }, { "fieldCaption": "Sales", "function": "SUM" } ] } }
// RESPONSE { "data": [ { "Region": "Central", "State": "Wisconsin", "SUM(Sales)": 32114.610000000022 }, { "Region": "Central", "State": "Texas", "SUM(Sales)": 170188.04579999982 }, { "Region": "Central", "State": "South Dakota", "SUM(Sales)": 1315.5600000000002 }, ... // MANY MORE RESULTS, I.E. ONE FOR EACH STATE IN THE DATA ] }
Based on the field I added, I will now get back a much larger array of objects (1 for each State) vs. the 4 (each Region) I got originally. If you’re working with very large datasets, make sure you understand the level of granularity you need for your use case(s) to avoid querying too much/not enough data!
Also, notice that – via the highlighted lines in the request above – we can easily add sorting to my results, as well!
Adding Custom Fields/Calculations
Your query can also define custom fields that don’t exist in your Datasource. This is super useful for creating quick ad-hoc calculations that are usually much faster in VizQL than they would be in your frontend or backend language.
There are some limitations to the calculations you can use with the API, but it uses the same syntax you know from Tableau Desktop and web authoring.
Check out this query:
// REQUEST { "datasource": { "datasourceLuid": "{{datasource_luid}}" }, "options": { "debug": true }, "query": { "fields": [ { "fieldCaption": "Customer Name" }, { "fieldCaption": "Sales", "fieldAlias": "Total Sales", "function": "SUM" }, { "fieldCaption": "Order Date", "fieldAlias": "Last Order Date", "function": "MAX" }, { "fieldCaption": "Days Since Last Order", "calculation": "DATEDIFF('day',MAX([Order Date]),MAX(TODAY()))" } ] } }
We’ve got Customer Name, SUM([Sales]), and MAX([Order Date]). Ok great, these are fields in the data and we’ve already seen aliasing. Look at the last field definition though – by using ‘calculation’ instead of ‘function’, we’re telling VDS that this field should be created on-the-fly! From that point, we can write an easy DATEDIFF calculation to get us ‘Days Since Last Order’
The results here look like this:
// RESPONSE { "data": [ { "Customer Name": "Carol Triggs", "Total Sales": 3241.8980000000006, "Last Order Date": "2021-09-25T00:00:00", "Days Since Last Order": 1312 }, { "Customer Name": "Lindsay Castell", "Total Sales": 3246.6260000000007, "Last Order Date": "2020-10-20T00:00:00", "Days Since Last Order": 1652 }, ... // MANY MORE RESULTS BY CUSTOMER ] }
To reiterate, not all calculation and aggregation types are available via the API. Be sure to refer to the documentation.
Applying Filters to Your Queries
In the real world, we don’t always want every single result from a query. Naturally, VizQL supports the ability to add filter definitions to your queries so you are in full control of the data it provides you.
A full background on filter types is beyond the scope of this post, so this is not an exhaustive list of filter capabilities. Refer to the documentation for additional examples, if needed.
We can use a SET filter to get results from a list of values:
// REQUEST { "datasource": { "datasourceLuid": "{{datasource_luid}}" }, "options": { "debug": true }, "query": { "fields": [ { "fieldCaption": "State" }, { "fieldCaption": "Sales", "function": "SUM" } ], "filters": [ { "filterType": "SET", "field": { "fieldCaption": "State" }, "values": [ "Ohio", "California" ], "exclude": false, "context": false } ] } }
- filters: An array of JSON objects that define how we want our date sliced and diced
- filterType: The type of filter to apply
- field: Which field should be evaluated
- values: An array of string values
- exclude (optional, default = false): If true, the values in the array will be removed from the results
- context (optional, default = false): If true, treat as a Context Filter
// RESULTS { "data": [ { "State": "California", "SUM(Sales)": 457687.63150000176 }, { "State": "Ohio", "SUM(Sales)": 78258.13599999995 } ] } // TWO TOTAL RESULTS
Next, let’s see how a QUANTITATIVE_NUMERICAL filter can get us Customers who’ve placed at least 3 orders:
// REQUEST { "datasource": { "datasourceLuid": "{{datasource_luid}}" }, "options": { "debug": true }, "query": { "fields": [ { "fieldCaption": "Customer Name" }, { "fieldCaption": "Order ID", "function": "COUNTD" } ], "filters": [ { "filterType": "QUANTITATIVE_NUMERICAL", "quantitativeFilterType": "MIN", "min": 3, "field": { "fieldCaption": "Order ID", "function": "COUNTD" } } ] } }
// RESPONSE { "data": [ { "Customer Name": "Carol Triggs", "COUNTD(Order ID)": 8 }, { "Customer Name": "Lindsay Castell", "COUNTD(Order ID)": 4 }, { "Customer Name": "Sanjit Jacobs", "COUNTD(Order ID)": 12 }, { "Customer Name": "Todd Boyes", "COUNTD(Order ID)": 5 }, ... // MANY MORE RESULTS ] }
Here’s an example of using a TOP N filter to get the five states with the highest sum of Sales:
// REQUEST { "datasource": { "datasourceLuid": "{{datasource_luid}}" }, "options": { "debug": true }, "query": { "fields": [ { "fieldCaption": "State" }, { "fieldCaption": "Sales", "function": "SUM", "sortPriority": 1, "sortDirection": "DESC" } ], "filters": [ { "filterType": "TOP", "field": { "fieldCaption": "State" }, "fieldToMeasure": { "fieldCaption": "Order ID", "function": "COUNTD" }, "direction": "TOP", "howMany": 5 } ] } }
// RESPONSE { "data": [ { "State": "California", "SUM(Sales)": 457687.63150000176 }, { "State": "New York", "SUM(Sales)": 310876.27099999995 }, { "State": "Texas", "SUM(Sales)": 170188.04579999982 }, { "State": "Pennsylvania", "SUM(Sales)": 116511.91399999999 }, { "State": "Illinois", "SUM(Sales)": 80166.10100000002 } ] } // FIVE TOTAL RESULTS
Multiple filter types for dates exist, as well. In this example, we’ll use NEXTN to filter for Orders placed in the first 3 months of 2020:
// REQUEST { "datasource": { "datasourceLuid": "{{datasource_luid}}" }, "options": { "debug": true }, "query": { "fields": [ { "fieldCaption": "Order ID" }, { "fieldCaption": "Order Date" } ], "filters": [ { "filterType": "DATE", "field": { "fieldCaption": "Order Date" }, "periodType": "MONTHS", "dateRangeType": "NEXTN", "rangeN": 3, "anchorDate": "2020-01-01" } ] } }
// RESPONSE { "data": [ { "Order ID": "CA-2020-146437", "Order Date": "2020-01-28T00:00:00" }, { "Order ID": "CA-2020-112123", "Order Date": "2020-03-03T00:00:00" }, { "Order ID": "CA-2020-128111", "Order Date": "2020-03-17T00:00:00" }, ... // MANY MORE RESULTS ] }
This is just scratching the surface of the different ways you can filter your queries. So – at the risk of sounding like a broken record – refer to the VDS documentation for a full list of options for how to filter and by what means! 🤓
Additional Options

Brevity Is The Soul of Wit (Disable Debug Mode)
By setting debug to false, you will receive more succinct responses. This can be useful in scenarios where total throughput is a concern for logging, auditing, etc. Note that you may not get useful information for troubleshooting errors, etc. Obviously, YMMV – so use at your own risk.
Querying Datasources that Require Credentials
You can use VDS with published Datasources that require authentication credentials! Simply supply them within the body of your query:
// REQUEST { "datasource": { "datasourceLuid": "YOUR-DS-LUID", "connections": [ { "connectionLuid": "CONNECTION-LUID", "connectionUsername": "YOUR-UID", "connectionPassword": "YOUR-PW" } ] }, "query": { // "fields": [ // Fields here ] } }
A couple of caveats here, though. First, the VizQL Data Service only supports user ID + password authentication. Additionally, it does not support Datasources with more than one connection that each require authentication.
For example, if your Datasource has one connection to Oracle which requires user ID + password and another connection to an Excel file, you’re in the clear. If that Datasource contains a connection to Oracle and another to Postgres that both require user ID + password, you won’t be able to query it with VDS as of the writing of this blog post.
OBJECTS vs ARRAYS
Most developer types will already be very familiar with the JSON format, which is the default for VDS. Some web development frameworks work better with an array of arrays (aka list of lists) versus an array of JSON objects. If this better suits your needs you can ask the API to respond in this format:
// REQUEST { "datasource": { // datasource info here }, "query": { "fields": [ // Fields here ] }, "options": { "returnFormat": "ARRAYS" } }
Compare the differences between the responses:
// OBJECTS RESPONSE (what we've seen so far) { "data": [ { "Ship Mode": "Second Class", "SUM(Sales)": 466671.11140000017 }, { "Ship Mode": "Standard Class", "SUM(Sales)": 1378840.5509999855 }, { "Ship Mode": "Same Day", "SUM(Sales)": 129271.955 }, { "Ship Mode": "First Class", "SUM(Sales)": 351750.73690000066 } ] } // ARRAYS RESPONSE { "data": [ [ "Second Class", 466671.11140000017 ], [ "Standard Class", 1378840.5509999855 ], [ "Same Day", 129271.955 ], [ "First Class", 351750.73690000066 ] ] }
Disaggregate Data
By default, VDS will aggregate data automatically based on your level of granularity and the functions/calculations you use. You can also ask VDS to return disaggregated data.
Easy-peasy. Just add to options:
// REQUEST { "datasource": { // datasource info here }, "query": { "fields": [ // Fields here ] }, "options": { "disaggregate": true } }
Bonus Gifts 🎁

If you’re just starting off with the VizQL Data Service, it’s critical to get a firm sense of how to structure your queries for best results…but let’s not kid ourselves: Writing a bunch of JSON over and over can be tedious. What if there were a more visual/UI-based way to build them?
I’ve built a free utility for VDS that can be used by anyone on Tableau Cloud! Just provide your pod, site and PAT. The app will query for Datasources for you to choose from. Next, it will walk you through selecting fields, creating custom calcs, adding filters, etc. At the end, you’ll be able to preview the results of your query and copy the JSON for use in your code.
Here’s a quick video of the tool in action:
It’s available here: http://kylejmassey.com/vds-utility
🔗 Click here to download a Postman collection containing all the requests from this post
Additional Videos
This is the video that accompanied my original VDS preview article from last year:
COMING SOON: I was pumped to present on VDS at Tableau Conference 2025 with fellow Ambassador Will Perkins. Unfortunately, the session, “VizQL and Chill”, was not recorded at TC – but not to worry! It will be recorded for a Tableau User Group event in late May 2025. I will add that video here as soon as it’s available.
This Is Where I Leave You…
As you may have noticed, I’m thrilled that the VizQL Data Service has finally made it to GA for all of Tableau Cloud and Server – and I’m hoping this post has made a fan out of you too! VDS is powerful, flexible, and finally ready for prime time — what will you build with it?
As always, I’d love to hear from you about how you might use the API and/or any feedback or questions you have. Please don’t hesitate to reach out by leaving a comment on this post or contacting me via one of my social links at the top and bottom of this page!
Happy Querying!