// Tableau + AI · REST API · TableauServerClient · Python

Automate Tableau Admin with Claude and the REST API

Use Claude to write TableauServerClient scripts: PAT sign-in, stale content, failed extract refreshes, a permissions audit and a dry-run bulk ownership change, plus the safety rules I follow before anything touches production.

OCT 14, 2026

Most Tableau admin work is the same handful of questions asked over and over. What hasn't been touched in a year? Which refreshes failed overnight? Who has access to what? This person left, who owns their content now? Every one of those is a short script against the Tableau REST API, and writing short scripts is something Claude does very well.

This guide is how I use Claude to write those scripts with TableauServerClient (TSC), the official Python library for the REST API. Four working examples, the safety rules I follow, and when it makes more sense to stop scripting and use a managed tool.

Why use Claude to write Tableau REST API scripts?

Because the hard part of admin scripting isn't the logic, it's the API details: which object has which attribute, which list calls are paginated, which fields can be filtered on the server. A model that knows TSC gets you past that in minutes. And if you use Claude Code, it can run the script, read the traceback and fix it in a loop, which is where most of the time goes.

What Claude doesn't know is your site. It doesn't know which projects are sacred, which service accounts own half your content, or that the "Sandbox" project is where everything goes to die. That's why every script below starts read-only, and why the safety rules come before the code.

Safety rules for AI-written Tableau admin scripts

I follow these every time, no exceptions:

  1. Read-only first. The first version of any script only reads and reports. It writes a CSV or prints a table. Nothing changes.
  2. Dry run by default. Any script that changes something has a --apply flag, and without it, the script only prints what it would do. A person reads that output before anyone adds the flag.
  3. Scope the identity. A personal access token (PAT) acts with the full rights of the user who created it. So the scoping happens at the user: run automation as a dedicated account with the lowest site role that does the job. Tableau's docs on personal access tokens cover creating and revoking them.
  4. Never paste secrets into a chat. Not the PAT, not a database password. Scripts read credentials from environment variables, and if Claude writes a token into the code, reject that version.
  5. Log every change. Before a script changes anything, it writes down the object, the old value and the new value. When something goes wrong at 2am, that file is the only thing that saves you.

I learned most of these the hard way. The longer version is in my field note, things I wish I'd known when I started building on TableauServerClient.

How do I authenticate to Tableau with a PAT in Python?

Install TSC with pip install tableauserverclient, create a PAT in your Tableau account settings, and keep the values in environment variables:

Python
import os
import tableauserverclient as TSC

auth = TSC.PersonalAccessTokenAuth(
    os.environ["TABLEAU_PAT_NAME"],
    os.environ["TABLEAU_PAT_SECRET"],
    site_id=os.environ.get("TABLEAU_SITE", ""),  # site content URL; "" = Default
)
server = TSC.Server(os.environ["TABLEAU_SERVER_URL"], use_server_version=True)

with server.auth.sign_in(auth):
    print(f"Signed in to site {server.site_id}")

use_server_version=True matches the client to your server's REST API version. Sign in once per run and reuse the server object. The TSC sign-in docs have the details.

Every example below assumes this block and runs inside the with.

How do I find stale content on Tableau Server or Cloud?

"Stale" needs a definition before it needs code. Here I mean a workbook that hasn't been updated since a cutoff date and whose views have no recorded views. The view count comes from the usage statistics the REST API can include with views.

Python
import csv
from collections import defaultdict
from datetime import datetime, timedelta, timezone

cutoff = datetime.now(timezone.utc) - timedelta(days=365)

# Total views per workbook, from view-level usage statistics
views_by_wb = defaultdict(int)
for view in TSC.Pager(server.views, usage=True):
    views_by_wb[view.workbook_id] += view.total_views or 0

# Only page through workbooks not updated since the cutoff
req = TSC.RequestOptions()
req.filter.add(TSC.Filter(
    TSC.RequestOptions.Field.UpdatedAt,
    TSC.RequestOptions.Operator.LessThan,
    cutoff.strftime("%Y-%m-%dT%H:%M:%SZ"),
))

with open("stale_workbooks.csv", "w", newline="") as f:
    out = csv.writer(f)
    out.writerow(["workbook", "project", "owner_id", "updated_at", "total_views"])
    for wb in TSC.Pager(server.workbooks, req):
        if views_by_wb[wb.id] == 0:
            out.writerow([wb.name, wb.project_name, wb.owner_id, wb.updated_at, 0])

Two notes. TSC.Pager walks every page for you; without it, you only get the first page and your report is quietly incomplete. And total_views is a lifetime count, so this finds content that has never been viewed. If you want "not viewed in the last 90 days," you need view timestamps from the activity data in Admin Insights or the server's admin views. Ask Claude to adapt the definition, and be explicit about which one you mean.

How do I list failed extract refreshes with Python?

The jobs endpoint returns background jobs, including extract refreshes, with their status. Filter for failures on the server, then keep the refresh job types:

Python
req = TSC.RequestOptions()
req.filter.add(TSC.Filter(
    TSC.RequestOptions.Field.Status,
    TSC.RequestOptions.Operator.Equals,
    "Failed",
))

refresh_types = {"refresh_extracts", "increment_extracts"}
for job in TSC.Pager(server.jobs, req):
    if job.type in refresh_types:
        print(job.ended_at, job.type, job.title, job.id)

Per the REST API reference for Query Jobs, only server and site administrators can call it, so this one needs an admin account. It also only covers the jobs the endpoint still returns, so treat it as "recent failures," not a full history. To see why a job failed, look it up with server.jobs.get_by_id(job.id).

This is a good script to run in a loop with Claude Code: ask it to group failures by data source and owner, then summarize the error patterns.

How do I audit Tableau permissions with TSC?

A common first audit: where does the All Users group have permissions at the project level? Permissions come back as a list of rules, each with a grantee and a dictionary of capabilities.

Python
# Find the All Users group's id
req = TSC.RequestOptions()
req.filter.add(TSC.Filter(
    TSC.RequestOptions.Field.Name,
    TSC.RequestOptions.Operator.Equals,
    "All Users",
))
groups, _ = server.groups.get(req)
all_users_id = groups[0].id

for project in TSC.Pager(server.projects):
    server.projects.populate_permissions(project)
    for rule in project.permissions:
        if rule.grantee.tag_name == "group" and rule.grantee.id == all_users_id:
            allowed = [cap for cap, mode in rule.capabilities.items() if mode == "Allow"]
            print(f"{project.name}: All Users can {', '.join(allowed)}")

This only reads. populate_permissions is one API call per project, so on a big site it takes a while; that's expected. Workbooks and data sources have the same populate_permissions method if you need to go a level deeper. When you eventually change permissions, remember they're add and delete operations on specific capabilities, not a "set these and leave the rest" call. Read, diff, change the minimum.

How do I bulk change workbook owners with Python?

The classic offboarding job: move everything one user owns to another. This is the first script here that writes, so it's a dry run unless you pass --apply, and it logs every change:

Python
import csv
import sys

APPLY = "--apply" in sys.argv

def find_user(name):
    req = TSC.RequestOptions()
    req.filter.add(TSC.Filter(
        TSC.RequestOptions.Field.Name,
        TSC.RequestOptions.Operator.Equals,
        name,
    ))
    users, _ = server.users.get(req)
    if len(users) != 1:
        raise SystemExit(f"Expected exactly one user named {name}, found {len(users)}")
    return users[0]

old_owner = find_user("departed.user@example.com")
new_owner = find_user("analytics.service@example.com")

with open("ownership_changes.csv", "a", newline="") as f:
    log = csv.writer(f)
    for wb in TSC.Pager(server.workbooks):
        if wb.owner_id != old_owner.id:
            continue
        print(f"{'CHANGE' if APPLY else 'WOULD CHANGE'}: {wb.project_name}/{wb.name}")
        if APPLY:
            log.writerow([wb.id, wb.name, old_owner.id, new_owner.id])
            wb.owner_id = new_owner.id
            server.workbooks.update(wb)

Run it without the flag, read every line, then run it with --apply. Data sources work the same way through server.datasources.update. The script refuses to guess when a name matches zero or several users, which is exactly what you want: user names aren't something to be clever about in a script that changes ownership.

How do I prompt Claude for a TSC script?

The prompt matters as much as it does for calcs. This is what I give it:

TEXT
Write a Python script using tableauserverclient (current version).
- Authenticate with PersonalAccessTokenAuth, reading the token name,
  secret, server URL and site from environment variables.
- Use TSC.Pager for every list call.
- Filter on the server with RequestOptions where the field supports it.
- Read-only: do not call any update, delete or publish method.
- Write results to a CSV and print a one-line summary.
Task: [describe the report you want]
If you are unsure whether a method or attribute exists in TSC, say so
instead of guessing.

That last line matters. Models occasionally invent a plausible TSC method. Check anything unfamiliar against the TSC API reference, and when TSC doesn't wrap an endpoint at all, have Claude call the REST API directly with the signed-in session, as I show in the field note.

When should I use a managed tool instead of scripts?

Scripts are perfect for one-off questions and periodic reports. They get harder when you want something watching all the time: catching a failed refresh before users do, noticing a stale extract, spotting permission drift, and telling the right person in plain English.

That's what I built Scout for. Scout is a monitor for Tableau Cloud (and Server) that watches refreshes, extracts and permissions, opens an incident with a diagnosis and the fix steps, and calls it in to Slack. Its monitoring tiers are read-only on Tableau, and the Autopilot tier can run low-risk fixes only within scopes you explicitly enable. If you'd rather keep scripting, keep scripting. If you find yourself scheduling five of these scripts and reading their output every morning, that's the point where a managed monitor pays off.

Next step

Set up the PAT and the sign-in block, then run the failed-refresh script against your own site. It's read-only, it's short, and it will tell you something you didn't know. For more on working with AI in Tableau, see AI for Tableau developers, the Tableau MCP server with Claude guide, or the Tableau + AI hub.

Frequently asked questions

How do I sign in to Tableau with a personal access token in Python?
With TableauServerClient: build TSC.PersonalAccessTokenAuth(token_name, token_secret, site_id=...), create TSC.Server(url, use_server_version=True), then use 'with server.auth.sign_in(auth):'. Read the token from environment variables, never from the code.
How do I list failed extract refreshes with TableauServerClient?
Filter the jobs endpoint on status Failed with RequestOptions, page through it with TSC.Pager(server.jobs, req), and keep jobs whose type is refresh_extracts or increment_extracts. Query Jobs requires a server or site administrator.
Is it safe to let Claude write Tableau admin scripts?
Yes, with rules: start read-only, put every write behind a dry-run flag that a person reviews first, run as a dedicated low-privilege account, keep secrets out of the chat, and log every change before making it.
How do I change workbook owners in bulk with Python?
Look up the old and new owners by name, page through workbooks with TSC.Pager, and for each workbook owned by the old user set wb.owner_id to the new user's id and call server.workbooks.update(wb). Run it as a dry run first and log each change.