Every tool registered by tableau-mcp-publish (registerAllTools in src/index.ts), with
parameters, return shape, guardrails, and a sample natural-language prompt that triggers it.
There are 27 tools in total, grouped into eight families below:
| Family | Count | Tools |
|---|---|---|
| Ingest & datasources | 6 | create_datasource_from_query, create_datasource_from_table, create_datasource_from_file, create_live_datasource, publish_datasource, publish_workbook |
| Design & build | 3 | design_dashboard, build_from_plan, create_starter_workbook |
| Branding | 1 | validate_brand |
| Metadata | 1 | get_datasource_fields |
| Scheduling & automation | 3 | schedule_refresh, list_refresh_schedules, delete_refresh_schedule |
| Webhooks | 3 | create_webhook, list_webhooks, delete_webhook |
| Pulse | 4 | create_pulse_definition, list_pulse_definitions, create_pulse_metric, delete_pulse_definition |
| Lifecycle & site management | 6 | list_projects, create_project, list_content, refresh_datasource, delete_content, set_permissions |
Publish tools require an explicit project (never auto-created), default overwrite to
false, and return the content LUID plus a Cloud URL. Destructive tools (delete_content,
delete_refresh_schedule, delete_webhook, delete_pulse_definition) require an explicit
confirm: true and throw otherwise — see each entry below.
Run a SQL query against a connection, materialize the result as a Hyper extract, package it as a
.tdsx, and publish it to a Tableau Cloud project.
| Param | Type | Required | Default | Notes |
|---|---|---|---|---|
connection |
object | yes | — | { type: "snowflake" | "postgres" | "csv", … }. For csv: { type, path }. |
sql |
string | yes | — | SQL to run (ignored for csv). |
datasourceName |
string | yes | — | Published datasource name. |
projectName |
string | yes | — | Target project (must exist). |
overwrite |
boolean | no | false |
|
maxRows |
number | no | 1,000,000 | Row cap. |
Returns: { datasourceLuid, url }
Guardrails: projectName must resolve to an existing project.
"Run
SELECT customer, region, revenue FROM sales ORDER BY revenue DESC LIMIT 100on Snowflake and publish it as 'Top Customers' in the Sales project."
Materialize a CSV file or an array of JSON records as a Hyper extract → .tdsx → publish.
| Param | Type | Required | Default | Notes |
|---|---|---|---|---|
csvPath |
string | no* | — | Local CSV path. |
records |
object[] | no* | — | Row objects (alternative to csvPath). |
datasourceName |
string | yes | — | Published datasource name. |
projectName |
string | yes | — | Target project. |
overwrite |
boolean | no | false |
* Exactly one of csvPath / records is required — the tool throws before any sidecar call
otherwise. Returns: { datasourceLuid, url }
"Publish this CSV as a datasource named 'Q2 Pipeline' in the Sales project."
Read a local file (CSV, JSON, JSONL, Excel .xlsx/.xls, or Parquet) into a Hyper extract and
publish it to Tableau Cloud as a .tdsx. File type is inferred from the extension when fileType
is omitted; unsupported extensions are rejected before the sidecar is called. Numeric- and
date-string columns are coerced to real types by the sidecar's ingest path (see
hyper_builder.py), so downstream tools like get_datasource_fields and Pulse pre-flight see
real NUMBER/DATE types instead of strings.
| Param | Type | Required | Default | Notes |
|---|---|---|---|---|
name |
string | yes | — | Datasource display name in Tableau. |
filePath |
string | yes | — | Absolute path to the source file on disk. |
fileType |
"csv" | "json" | "jsonl" | "xlsx" | "xls" | "parquet" |
no | inferred | Override inferred extension. |
excelSheet |
string | integer | no | first sheet | Excel only: sheet name or 0-based index. |
jsonPath |
string | no | top-level array | JSON/JSONL only: single-level JSONPath selector (e.g. $.data). |
projectName |
string | yes | — | Target project (must already exist). |
overwrite |
boolean | no | false |
Overwrite an existing datasource with the same name. |
Returns: { datasourceLuid, contentUrl, url }
Guardrails:
- Unsupported file extensions (anything other than the six listed above) throw before any sidecar call.
projectNamemust resolve to an existing project; the server never auto-creates projects.
"Publish
sales.parquetas a datasource named 'Regional Sales' in the Analytics project."
Publish a LIVE Cloud connection (Snowflake or Presto/Trino) — no extract, no locally
materialized data. The sidecar builds a .tds that carries only connection topology
(server/warehouse/schema/database/table); credentials are embedded separately at publish time
via Tableau's <connectionCredentials> element (src/rest/credentials.ts) and are never written
into the .tds file or logged.
| Param | Type | Required | Default | Notes |
|---|---|---|---|---|
name |
string | yes | — | Datasource display name in Tableau. |
projectName |
string | yes | — | Target project (must already exist). |
connection.type |
"snowflake" | "presto" |
yes | — | Discriminates the rest of connection. |
connection.server |
string | yes | — | Account/coordinator host. |
connection.schema |
string | yes | — | Schema name. |
connection.table |
string | yes | — | Table this datasource binds to. |
connection.warehouse / .dbname |
string | yes (snowflake) | — | Snowflake-only. |
connection.authentication |
string | no | "username-password" |
"username-password" or "oauth". Key-pair auth throws (see guardrails). |
connection.role |
string | no | — | Snowflake-only optional role. |
connection.port |
number | no | 8080 | Presto-only. |
connection.catalog |
string | yes (presto) | — | Presto-only. |
connection.ssl |
boolean | no | true |
Presto-only. |
connection.useRemoteQueryAgent |
boolean | no | true |
Presto-only — assume Tableau Bridge is needed unless the endpoint is internet-reachable and allowlisted. |
credentials.username / .password |
string | yes | — | Source from environment; never hardcode or log (see caveats). |
overwrite |
boolean | no | false |
Returns: { datasourceLuid, url, note } — note always restates the connectivity caveat for
the connection type used.
Guardrails / honest caveats:
- Snowflake key-pair authentication is IMPOSSIBLE over REST — Tableau's Publish Datasource API
only supports embedding username/password or OAuth credentials. The tool throws a clear,
actionable error (
"…is not REST-publishable…") rather than silently emitting XML that will never authenticate; configure key-pair auth in Tableau Desktop and publish from there instead. - Presto/Trino is generally Tableau-Bridge-dependent on Cloud — Cloud usually cannot reach a
private-network Presto cluster directly.
useRemoteQueryAgentdefaults totrue; set itfalseonly when the endpoint is internet-reachable and allowlisted. - VERIFY-LIVE: the exact XML attribute spelling for both connector classes
(
sidecar/tds_builder.py'sSNOWFLAKE_ATTRS/PRESTO_ATTRS) is this project's best-documented mapping and has not yet been confirmed against a Desktop-exported.tds— treat as best-effort until verified against a live site. - Credentials never touch the
.tdsfile, logs, or error messages (asserted intests/credentials.test.ts).
"Publish a live Snowflake connection to the ORDERS table in the SALES schema of the ANALYTICS database, warehouse COMPUTE_WH, as 'Live Orders' in the Sales project — use username/password auth from my environment."
Publish an existing local file (.tdsx/.hyper or .twb/.twbx). Chunked upload is used
automatically for files 64 MB or larger.
| Param | Type | Required | Default | Notes |
|---|---|---|---|---|
filePath |
string | yes | — | Local file path. |
name |
string | yes | — | Published name. |
projectName |
string | yes | — | Target project. |
overwrite |
boolean | no | false |
Returns: { datasourceLuid \| workbookLuid, url }
Generates a DashboardProposal (kind: "proposal") once enough context is known, or a
ClarifyingQuestions object (kind: "questions") in interview mode, from a business question,
target audience (or named persona), and optional field hints. This tool never builds or
publishes anything — building is always a separate, explicit step. All planning is deterministic
and stateless: the same inputs always produce the same output.
| Param | Type | Required | Default | Notes |
|---|---|---|---|---|
mode |
"autonomous" | "interview" | "interview_followup" | "directed" |
yes | — | Planning mode (see below). |
audience |
"exec" | "analyst" | "operational" | "mixed" |
no | "mixed" (or persona base) |
Required for autonomous/directed/interview_followup unless persona is supplied. |
persona |
string | no | — | Named persona from brand.yaml (e.g. "ceo", "cto", "analyst"). Resolves the persona's base audience (used when audience is omitted) and applies its overrides (maxSheets, chartDeny, kpiEmphasis, preferredArtifact, tone). An explicit audience always wins over the persona's base. |
brandPath |
string | no | repo-root brand.yaml |
Only used when persona is supplied. |
businessQuestion |
string | no | — | Required for autonomous/interview_followup. |
directions |
string | no | — | Required for directed — throws if empty/absent. |
answers |
Record<string, string> |
no | — | Required for interview_followup — map of question IDs to answers from a preceding interview call. |
fieldHints |
FieldHint[] |
no | — | { name, role?, dataType? }[] from the target datasource. Omitted → placeholder tokens in the plan. Get real ones from get_datasource_fields. |
datasourceLuid / datasourceName / projectName |
string | yes* | — | Required for autonomous/directed/interview_followup. |
workbookName |
string | no | auto-derived | |
requestedLayout |
"tiled_vertical" | "tiled_horizontal" |
no | audience default | Ignored for operational (always tiled_vertical). |
autonomous — Derive sheets and layout entirely from businessQuestion + fieldHints.
interview — Return 3–7 clarifying questions without producing a plan/proposal. Selected
deterministically based on which inputs are unknown. Does not require datasourceLuid.
interview_followup — Consume answers from a preceding interview call and produce a full proposal.
directed — Map explicit directions to concrete sheet specs.
- Agent calls
design_dashboard(autonomous/directed/interview_followup) → server returnskind: "proposal". - Agent presents the proposal to the user:
summary,kpiStrip,views,layoutSummary,storyOutline(if present). - User says "change X" → agent re-calls
design_dashboardindirectedmode with updateddirectionsfor a fresh proposal — no session state is stored server-side. - User says "confirm" → agent calls
build_from_planwithproposal.planverbatim (do not modify it first).
interview mode:
{ "schemaVersion": 1, "kind": "questions", "questions": [{ "id": "q_goal", "question": "…", "hint": "…" }] }All other modes return a DashboardProposal (wrapped as { result: <proposal> } in the tool
response):
{
"schemaVersion": 1,
"kind": "proposal",
"workbookName": "Revenue by Region",
"audience": "exec",
"datasourceName": "Regional Sales",
"projectName": "Analytics",
"summary": "Exec dashboard on \"Regional Sales\" featuring a KPI band of 3 metrics …",
"kpiStrip": [{ "label": "Sales", "primaryMeasure": "Sales", "deltaMeasure": "Sales Difference", "direction": "up_good" }],
"views": [{ "title": "Sales by Category", "chartType": "bar", "encodingSummary": "bar: Sales by Category, colored by Segment", "fields": ["Sales", "Category", "Segment"] }],
"layoutSummary": "KPI band of 3 tiles … above a 2-chart row (…).",
"storyOutline": ["Headline KPIs", "Profit", "Sales by Category"],
"openQuestions": ["…"],
"plan": { "schemaVersion": 1, "kind": "plan", "…": "…" }
}proposal.plan is the input DashboardPlan embedded verbatim — pass it unchanged to
build_from_plan on user confirmation.
| Audience | Max sheets | Canvas | Allowed mark types | KPI required | Max measures/sheet | Max dims/sheet |
|---|---|---|---|---|---|---|
exec |
3 | 1000×800 | bar, line, text, map_filled (no bare scatter) | yes (index 0) | 1 | 1 |
analyst |
8 | 1200×900 | bar, line, text, map, scatter, map_filled | no | 4 | 3 |
operational |
6 | 800×1200 | text, bar, line | yes (index 0) | 2 | 1 |
mixed |
6 | 1000×900 | bar, line, text | no | 2 | 2 |
A persona's maxSheets/chartDeny/kpiEmphasis overrides apply on top of these base constraints
(BI_DESIGN §9.1) — e.g. persona: "ceo" caps maxSheets at 3 and denies no extra chart types by
default; persona: "client" denies scatter/map.
directedmode throws immediately ifdirectionsis empty or absent.- Non-interview modes without
datasourceLuid/datasourceName/projectNamethrow. - Unknown
personanames throw, listing every available persona frombrand.yaml. - When
fieldHintsis omitted, sheets contain placeholder tokens ("<measure>","<dimension>") and the proposal'sopenQuestionscalls this out;build_from_planrejects these tokens. - A persona whose
preferredArtifactis"pulse"gets an honestopenQuestionsnote — Pulse metric generation is a separate tool family (below), not somethingdesign_dashboardproduces.preferredArtifact: "story"is fully supported (seestoryArc/storyOutline).
"Design an exec dashboard for the 'ceo' persona answering 'How is revenue trending by region?' — use autonomous mode."
"I'm not sure what I need. Ask me clarifying questions before designing the dashboard."
The only side-effecting tool in the design/build family. Consumes a DashboardPlan (from
proposal.plan), builds a self-contained .twbx with all worksheets tiled inside a <dashboard>
element (plus an optional <dashboard type='storyboard'> when the plan carries a storyArc), and
publishes it to Tableau Cloud.
The workbook embeds the .hyper extract directly (federated connection), so it renders on Tableau
Cloud without depending on a separately published datasource binding. A governed .tdsx datasource
is also published as an independent artifact.
Tableau Cloud renders a workbook only when all datasource bindings can be resolved at publish time.
The only reliable path is embedding the .hyper extract directly (federated connection), which
requires the datasource to be materialized from a local file in the same build_from_plan call.
| Scenario | Error |
|---|---|
Plan has no datasourceSpec (LUID-only) |
Throws before any sidecar/REST work — binding via datasourceLuid alone produces a non-rendering workbook (Tableau Cloud error 400011). |
datasourceSpec.sql (query branch) |
Throws — the query path produces a .tdsx but no standalone .hyper to embed. |
The only supported path is datasourceSpec.filePath — a local CSV, JSON, JSONL, Excel, or
Parquet file.
| Param | Type | Required | Default | Notes |
|---|---|---|---|---|
plan |
object | yes | — | The DashboardPlan from proposal.plan. Must have schemaVersion: 1, kind: "plan", and datasourceSpec.filePath. |
overwrite |
boolean | no | false |
Overwrite an existing workbook (and datasource) with the same name. |
brandPath |
string | no | repo-root brand.yaml |
Applies branding when plan.personaName/plan.brandName is set (i.e. the plan came from a persona-resolved design_dashboard call) or when explicitly passed. Branding is skipped entirely when none of the three are present. |
{ workbookLuid, url, datasourceLuid } — datasourceLuid is always present (the filePath path
always creates a new datasource).
- Missing
datasourceSpecordatasourceSpec.filePaththrows before any sidecar/REST call. - Placeholder tokens (
<measure>/<dimension>) in any sheet field throw before any work begins. - Audience invariants (mark type, measure count, dimension count) are re-validated at build time — defense in depth against a hand-edited plan.
storyArc[].capturedSheetreferences are re-validated against the plan's own sheet titles before calling the sidecar (which validates a third time against the actual built workbook).plan.projectNamemust resolve to an existing project.overwritedefaults tofalse.
"Publish the confirmed proposal — build and publish that dashboard now."
design_dashboard's deterministic storyArc captions (docs/feature-prompt-authoring/ BI_DESIGN.md §9.2) are a template FLOOR, not the intended final copy — the calling agent is
expected to rewrite them with real narrative before calling build_from_plan. capturedSheet
values must stay untouched (they must remain one of plan.sheets[].title); only caption text
changes:
Dropping or reordering points is also fine — every remaining capturedSheet just has to name
an existing plan.sheets[].title. Inventing a capturedSheet that doesn't exist throws before
any sidecar/REST call (the guardrail above).
Generate a .twbx bound to an existing published datasource (one worksheet per sheet spec, via
sqlproxy/repository-location — no embedded extract) and publish it. Mark types bar, line,
text are fully supported; map is experimental.
| Param | Type | Required | Default | Notes |
|---|---|---|---|---|
datasourceLuid |
string | yes | — | LUID of the published datasource. |
datasourceName |
string | yes | — | Its display name. |
workbookName |
string | yes | — | Published workbook name. |
projectName |
string | yes | — | Target project. |
sheets |
object[] | yes | — | { title, markType, rows[], cols[], measures[] }. |
overwrite |
boolean | no | false |
Returns: { workbookLuid, url }
"Build a starter workbook on the Top Customers datasource with a bar chart of revenue by region."
Loads and validates the brand kit file (brand.yaml) that drives palette, typography, number
formats, and named personas used by design_dashboard. Never throws on an invalid or missing
file — always returns a result so problems can be surfaced to the user instead of failing the call.
| Param | Type | Required | Default | Notes |
|---|---|---|---|---|
path |
string | no | repo-root brand.yaml |
Optional override path. |
Returns: { valid, warnings: string[], personas: string[], summary }
Guardrails: an absent file returns valid: true with a warning (built-in defaults apply — see
DEFAULT_BRAND in src/branding/schema.ts); a malformed/schema-invalid file returns
valid: false with the field-level issues in warnings, never an exception.
"Validate my brand.yaml and list the personas it defines."
Fetch the real field names, captions, data types, and default aggregations for a published
datasource via the VizQL Data Service (read-metadata). Use this before design_dashboard so its
fieldHints reference fields that actually exist, and as a pre-flight check before
create_pulse_definition.
| Param | Type | Required | Default | Notes |
|---|---|---|---|---|
datasourceLuid |
string | yes | — | LUID of a published datasource. |
Returns: { fields: [{ name, caption?, dataType?, defaultAggregation? }], count }
Guardrails: a 404 is rewrapped with a hint to confirm the LUID and that the datasource is published (not local/embedded-only); a "feature disabled" response is rewrapped with a hint that VDS may not be enabled for the site. GET is safely retried on 429/502/503/504.
"What are the real field names on the Regional Sales datasource?"
Tableau Cloud has no classic Server-style shared schedules over REST — each task carries its
own embedded frequency + interval schedule. These tools are distinct from refresh_datasource
(below), which triggers a single immediate refresh with no recurring schedule.
Create a recurring extract-refresh task on Tableau Cloud for a published datasource or workbook.
| Param | Type | Required | Default | Notes |
|---|---|---|---|---|
targetType |
"datasource" | "workbook" |
yes | — | |
targetId |
string | yes | — | LUID of the target. |
type |
"FullRefresh" | "IncrementalRefresh" |
no | "FullRefresh" |
IncrementalRefresh's exact token spelling is VERIFY-LIVE — prefer FullRefresh until confirmed. |
frequency |
"Hourly" | "Daily" | "Weekly" | "Monthly" |
yes | — | |
frequencyDetails |
object | yes | — | { start: "HH:MM:SS", end?, intervals[] }. Hourly needs ≥1 interval with hours; Weekly needs ≥1 with weekDay; Monthly needs exactly one with monthDay; Daily needs none. |
Returns: { taskId, frequency?, nextRunAt?, note }
Guardrails / honest caveat (always included in note, not just on failure — it can't be
reliably detected from the API response): Tableau Cloud can only execute this schedule for a
connection that is cloud-reachable with embedded credentials (e.g. Snowflake via
create_live_datasource). A datasource published from a local file (CSV/Excel/local Hyper
extract) accepts the schedule-creation call successfully, but Cloud cannot refresh a file-based
extract without Tableau Bridge — the task may be created while every run fails. Use the
local-file cron design-around for file-based datasources instead.
"Schedule a daily 3am refresh of the Live Orders datasource."
List every extract-refresh task on the site.
Returns: { schedules: [{ taskId, type, targetKind?, targetId?, frequency?, nextRunAt? }] }
"List every scheduled refresh on this site."
Delete a recurring extract-refresh task by taskId. Destructive and irreversible.
| Param | Type | Required | Default | Notes |
|---|---|---|---|---|
taskId |
string | yes | — | From schedule_refresh / list_refresh_schedules. |
confirm |
boolean | no | false |
Must be explicitly true; deletion is refused otherwise. |
Returns: { deleted, taskId }
"Delete refresh schedule task-abc123 — yes I'm sure."
Create a site webhook that POSTs to an HTTPS destination when a Tableau event fires.
| Param | Type | Required | Default | Notes |
|---|---|---|---|---|
name |
string | yes | — | Human-readable webhook name. |
event |
enum | yes | — | One of Datasource{RefreshStarted,RefreshSucceeded,RefreshFailed,Created,Updated,Deleted} or the Workbook* equivalents. |
url |
string | yes | — | Destination URL. Must be HTTPS — the tool rejects http:// before any REST call. |
Returns: { webhookId, name, event }
Guardrails: requires site-administrator privileges on the signed-in PAT — a 403 is rewrapped
with that explanation rather than a bare "Forbidden". The destination receives
{ resource, event_type, resource_name, site_luid, resource_luid, created_at }.
"Create a webhook that posts to https://example.com/hooks/tableau when a datasource refresh fails."
List every webhook configured on the site.
Returns: { webhooks: [{ webhookId, name, event, url? }] }
Delete a webhook by webhookId. Destructive and irreversible.
| Param | Type | Required | Default | Notes |
|---|---|---|---|---|
webhookId |
string | yes | — | From create_webhook / list_webhooks. |
confirm |
boolean | no | false |
Must be explicitly true. |
Returns: { deleted, webhookId }
Tableau Pulse is Cloud-only (not available on Tableau Server) and must be enabled for the site
under Site Settings → Pulse. This note is always surfaced on create_pulse_definition's output —
Pulse enablement/permission failures can't be reliably distinguished from other 404s.
Honest status (2026-07-17): all four tools are code-complete against Tableau's official
pulse-api-utilitiesreference payload shape, with a VDS-backed pre-flight check. Live proof is blocked:POST /api/-/pulse/definitionsreturns a bare 400 against the real API — thebasic_specificationinternals are specified-by-example only in Tableau's own reference repo (which clones, never constructs, that block from scratch), so the exact wire shape is not yet locked against a live fixture. Every enum beyond the few tokens the research brief explicitly confirms is marked VERIFY-LIVE insrc/rest/pulse.ts. Treat these tools as best-effort until a definition is created once in the Pulse UI, fetched back via GET, and used to correct the client. 2026-07-20 addendum: an external agent-skill cross-check (docs/adr/0014-external-skill-analysis.md) added purely additive, VERIFY-LIVE enum/field widening (currencyCode,insightSettings,rowLevelIdField/rowLevelNameField/rowLevelEntityNames, fiscal granularity/comparison tokens) — this is schema-readiness only and does not close the live-blocked 400 above.
Create a Tableau Pulse metric definition.
| Param | Type | Required | Default | Notes |
|---|---|---|---|---|
name |
string | yes | — | Display name for the definition. |
datasourceLuid |
string | yes | — | LUID of a single published datasource with a real date dimension. |
measure.field |
string | yes | — | Measure field name/caption, e.g. "Sales". |
measure.aggregation |
"SUM" | "AVERAGE" | "MEDIAN" | "MAX" | "MIN" | "COUNT" | "COUNT_DISTINCT" |
yes | — | |
timeDimension.field |
string | yes | — | Date-like field name/caption, e.g. "Order Date". |
filters |
object[] | no | [] |
Specification filters — shape VERIFY-LIVE. |
allowedDimensions |
string[] | no | [] |
Dimensions Pulse may use for insight breakdowns. |
numberFormat |
"NUMBER" | "CURRENCY" | "PERCENT" |
no | "NUMBER" |
|
sentiment |
"NONE" | "UP_IS_GOOD" | "DOWN_IS_GOOD" |
no | "NONE" |
|
isRunningTotal |
boolean | no | false |
|
currencyCode |
"USD" | "EUR" | "GBP" | "JPY" | "UNSPECIFIED" |
no | — | VERIFY-LIVE (schema-readiness only — see honest status note). Emitted as representation_options.currency_code only when set. |
insightSettings |
{ type, disabled? }[] |
no | [] |
VERIFY-LIVE. type is one of the 8 INSIGHT_TYPE_* values (CURRENT_TREND, NEW_TREND, TOP_DRIVERS, TOP_DETRACTORS, BOTTOM_CONTRIBUTORS, RISKY_MONOPOLY, UNUSUAL_CHANGE, RECORD_LEVEL_OUTLIERS). Emitted as insights_options.settings entries. |
rowLevelIdField / rowLevelNameField |
string | no | — | VERIFY-LIVE. Identifier/label columns enabling INSIGHT_TYPE_RECORD_LEVEL_OUTLIERS. |
rowLevelEntityNames |
{ singular, plural } |
no | — | VERIFY-LIVE. Emitted as specification.row_level_entity_names.{singular_noun,plural_noun}. |
skipPreflight |
boolean | no | false |
Skip the VDS pre-flight field check (e.g. when VDS itself is known unavailable). |
Returns: { definitionId, name, note }
Guardrails:
- Runs a VDS-backed pre-flight check before calling Pulse: verifies
measure.fieldandtimeDimension.fieldexist on the datasource (throws, listing every available field, on a mismatch) and that the time dimension's VDSdataTypelooks date-like; warns (without blocking) when the measure's VDSdefaultAggregationdisagrees with the requested aggregation. If VDS itself fails, pre-flight is automatically skipped with a warning rather than blocking creation. - Requires
tableau:insight_definitions:create/updatePAT scope, and write+publish permission on the target datasource. - See the honest status note above — live creation currently returns a 400.
"Create a Pulse metric definition tracking monthly Sales, aggregated as SUM, using Order Date as the time dimension."
List every Pulse metric definition on the site.
Returns: { definitions: [{ definitionId, name, datasourceLuid? }], count }
Guardrails: requires tableau:insight_definitions_metrics:read PAT scope.
Create a Pulse metric instance from an existing definition.
| Param | Type | Required | Default | Notes |
|---|---|---|---|---|
definitionId |
string | yes | — | From create_pulse_definition / list_pulse_definitions. |
filters |
object[] | no | [] |
Shape VERIFY-LIVE. |
granularity |
enum | no | "GRANULARITY_BY_MONTH" |
Only this default is confirmed against Tableau's official reference; other values are VERIFY-LIVE. |
range |
enum | no | "RANGE_LAST_COMPLETE" |
Same caveat. |
comparison |
enum | no | "TIME_COMPARISON_PREVIOUS_PERIOD" |
Same caveat. |
Returns: { metricId }
Guardrails: requires tableau:insight_metrics:create PAT scope. Prefer the defaults until the
live fixture confirms other enum values.
Delete a Pulse metric definition by id. Destructive and irreversible — also removes any metrics built on it.
| Param | Type | Required | Default | Notes |
|---|---|---|---|---|
definitionId |
string | yes | — | |
confirm |
boolean | no | false |
Must be explicitly true. |
Returns: { deleted, definitionId }
Guardrails: the exact delete response shape is unconfirmed against a live site (VERIFY-LIVE) — per Tableau's own guidance, create one definition in the UI and GET it back to lock a fixture before relying on this in production.
list_projects → { projects: [{ id, name }] }.
create_project({ name, description? }) → { id, name }.
"What projects do I have?" / "Create a project called Analytics Sandbox."
list_content→{ items: [{ id, name, type, projectName?, updatedAt? }] }refresh_datasource({ datasourceId })→{ ok, datasourceId }— a single immediate refresh, not a recurring schedule (seeschedule_refreshabove for recurring).delete_content({ contentType, luid, confirm })→{ deleted, contentType, luid }. Destructive — requiresconfirm: true, and additionally refuses to delete content that lives in theDefaultproject (governance guardrail — checked vialist_contentbefore the delete call).
"List my published content." / "Refresh the Top Customers extract." / "Delete workbook
<luid>— yes I'm sure."
Grant or deny capabilities on a published datasource or workbook.
| Param | Type | Required | Default | Notes |
|---|---|---|---|---|
contentType |
"datasource" | "workbook" |
yes | — | |
contentId |
string | yes | — | LUID. |
grants |
object[] | yes | — | { granteeId, granteeType?: "user"|"group", capability, mode: "Allow"|"Deny" }. |
confirmElevated |
boolean | no | false |
Required when any grant uses an elevated capability. |
Returns: { ok, grantsApplied }
Guardrails:
- Capabilities are validated against an 18-entry allowlist (
Read,Write,Connect,ExportData,ChangePermissions,Delete,ProjectLeader,ChangeHierarchy, …) — an unknown capability throws, listing the allowlist. - Elevated capabilities (
ChangePermissions,Delete,ProjectLeader,ChangeHierarchy) requireconfirmElevated: true; otherwise the call throws before any REST work.
"Grant Read access on the Top Customers datasource to the Sales group."
Deterministically score a generated dashboard plan against the mined top-100
business_dashboard norms (design/corpus/stats/visual_norms.yaml), the WCAG contrast math
(src/branding/contrast.ts), and the ENFORCED Stephen Few rules. No LLM — this is the
architecture-honest assimilation of the external skill suite's VizCritique-Pro (whose LLM judge would violate the
A-01 no-LLM-in-server invariant). Dimensions: layout, KPI count, chart mix + Few, title, contrast.
A dimension whose backing norm bucket is confidence: low degrades to a note (excluded from the
score). Returns { overallScore, dimensions: [{ dimension, verdict: pass|warn|note, finding, normCited? }] }.
Derive a structured metric dictionary for a published datasource from its VDS field metadata
(reuses the get_datasource_fields fetch and the planner's own field-role inference). Per field:
{ name, caption?, role: measure|dimension|date, suggestedAggregation?, format: currency|percent|number|text, definitionTemplate }.
Deterministic; the natural-language prose is left to the calling agent (the external skill suite's Scribe Metric-Builder,
architecture-honest subset). Returns { metrics, count }.
A read-only governance-lite site audit (the external skill suite's Governance-Scanner, deterministic subset) over the
existing list_content surface. Three checks: stale (not updated in staleDays, default 180),
Default-project residence, and naming hygiene. Owner-concentration, adoption, and certification
checks are intentionally out of scope — they need read introspection this publish-oriented server does
not carry (see ADR-0014). Returns { findings, summary }.