powerbi-cli
A command line tool for Power BI admins that saves workspaces, apps, reports, report users and workspace scans as JSON and Excel files: auth profiles for user and admin tokens, a versioned offline cache, file converters and batch scans from a YAML config, with every API call it makes written down.
Build this pack
Paste this into Claude Code, Codex or Cursor, with your language. Your agent runs every step below on its own, builds the product the pack describes, and stops when it works and the pack's checks pass.
Build https://symbola.sh/packs/emptymalei/powerbi-cli/ in Python The steps, to run yourself or to see what your agent does
Install the Symbola CLI (Python 3.11+):
uv tool install symbola(orpipx install symbola). Notpip install symbolon: that PyPI name is another project.Download the pack:
symbola pull emptymalei/powerbi-cli --dest spec.Read
spec/AGENTS.mdand follow it. Build only fromspec/: don't look for the original product's source, the registry repository (it holds holdout tests) or other builds. Packs released before the rename saysymbolon <command>: runsymbola <command>instead. This is a product pack:spec/PRODUCT.mddescribes the whole product (its shape, surfaces and flows). Build that product, in the user's stack, with the components below as its core. Don't build a demo page around the components instead.Build each component in its own folder and run its checks as you go, fixing what fails:
symbola impl-init impl/auth-profiles --contract spec/components/auth-profiles/versions/1 symbola test spec/components/auth-profiles/versions/1 impl/auth-profilesIn Python, define
run(input)inimpl.py: it takes and returns a dict, and raisesValueErrorfor input the contract rejects. Pin third-party packages with==inrequirements.lock. In another language, setlanguage = "command"andcommand = [...]insymbolon-impl.toml; the program reads one JSON input per line on stdin and writes{"output": ...}per line. A component whosesymbolon.tomlsaysdriver = "cli"is a command line: build the real program, setcommandto it, and symbola runs it once per check.When every component passes, run
symbola keygenonce, then for each componentsymbola attest spec/components/auth-profiles/versions/1 impl/auth-profiles -o attestations/auth-profiles.json. On macOS, attest needs Docker running and accepts Python, Node or a static Linux binary (for Go,GOOS=linux);symbola testruns the same checks without it.
You are done when every flow in spec/PRODUCT.md works in the product you built and attest reports PASSED for every component. Write open questions and the readings you chose in NOTES.md. Every command is described at https://kausalflow.github.io/symbola/cli/.
Components:
auth-profiles:spec/components/auth-profiles/versions/1cache-keys:spec/components/cache-keys/versions/1cached-reads:spec/components/cached-reads/versions/1config-cache:spec/components/config-cache/versions/1file-converters:spec/components/file-converters/versions/1flatten:spec/components/flatten/versions/1scan-batch:spec/components/scan-batch/versions/1workspace-entries:spec/components/workspace-entries/versions/1
Every command: Symbola CLI docs. Agents can read these steps as plain text in llms.txt.
Who has built it
Test results that ship with the pack, one column per build. 8 of 8 components pass a build · 1 build · 1 runner.
| Component | emptymalei |
|---|---|
auth-profiles | ✓ 45 checks |
cache-keys | ✓ 17 checks |
cached-reads | ✓ 22 checks |
config-cache | ✓ 39 checks |
file-converters | ✓ 25 checks |
flatten | ✓ 11 checks |
scan-batch | ✓ 26 checks |
workspace-entries | ✓ 15 checks |
Runners: runner-i · newest 2026-10-28. Hover a result for details.
What a build must do
Each one is a decision a builder could get wrong. Decided is required, default may change in a fork with a note, and open is deliberately left to the builder.
auth
- A token given as `Bearer <token>` is stored without the prefix. auth.token-prefix · 2 checks
- Without `-g` a saved profile becomes the ungrouped active profile when none is active, or when it is named `default`; with `-g` it becomes the group's active profile when the group has none. An existing active profile otherwise stays, and a grouped save leaves the ungrouped profiles alone. auth.activation · 6 checks
- Tokens are kept once per profile name in `~/.pbi_cli/credentials.json` (a JSON object, indent 2), shared by the ungrouped and grouped profiles of that name: saving replaces the token, deleting a profile deletes it, even if another list still has a profile of that name. auth.token-per-name · 5 checks
- The credentials file is readable and writable only by its owner (mode 600). auth.credentials-private · 0 checks
- When an OS keyring works, tokens are stored there (service `pbi-cli`, user name = profile name) and read from there first; the credentials file is only the fallback, also when the keyring raises any error. auth.keyring · 0 checks
- Old installations keep working: a legacy `profiles.json` is imported into a new `config.yaml`, and a legacy `auth.json` token becomes the ungrouped profile `default` when there are no ungrouped profiles. auth.legacy-import · 2 checks · default
- A command uses the active profile of its group (`user`, or `admin` for the admin-only commands) if that is one of the group's profiles; otherwise the ungrouped active profile, which must be one of the ungrouped profiles; it then needs a token for it. A group profile without a token does not fall back. Failures exit 1 with an `Error:` line naming the group or profile. auth.resolution · 13 checks
profile
- `profile list` shows the ungrouped profiles (✓ when a token is stored under the name, ✗ when not, `(active)` on the active one) and then each group, with a hint for each empty part. profile.list · 4 checks
- `profile switch NAME [-g G]` makes NAME the active profile of that list; an unknown NAME, or an empty list, is reported and changes nothing, with exit 0. profile.switch · 7 checks
- Without a name, `profile switch` lists the profiles numbered from 1, reads the number from standard input (an empty answer is 1) and switches; a number out of range prints `Invalid selection` and changes nothing. profile.switch-interactive · 3 checks · default
- `profile delete NAME [-g G]` asks for confirmation (`--yes` skips it; declining aborts with exit 1), removes the profile and its token, and when it was the active one activates the first remaining profile of that list (none if none remain), announcing it. An unknown name is reported with exit 0. profile.delete · 8 checks
config
- `~/.pbi_cli/config.yaml` is a block-style YAML mapping with `active_profile`, `profiles`, `groups` (`user`, `admin`, each with `active_profile` and `profiles`), `default_output_folder`, `cache_folder` and `cache_enabled` (default true). A missing or unreadable file reads as the defaults; a change rewrites the whole file and keeps all other keys as they were. config.file · 7 checks
- `config set-output-folder P` and `set-cache-folder P` drop surrounding quotes from P, expand `~` to the home directory, make a relative path absolute from the current directory and store it without a trailing slash; they print `✓ ... set to: <path>` and a hint line. config.set-folder · 5 checks
- A cache folder starting with `s3://`, `gs://` or `az://` is stored and shown as given. config.cloud-cache-folder · 3 checks
- `get-output-folder` and `get-cache-folder` print the stored folder (the cache one also `Cache enabled: True|False`) or say it is not configured; `enable-cache` and `disable-cache` set `cache_enabled` and print `✓ Cache enabled` / `✓ Cache disabled`. config.get-and-switch · 7 checks
- `config show` prints the active ungrouped profile, default output folder, cache folder, whether the cache is enabled, the profile count, the ungrouped profiles, and per group its profile count and active profile. config.show · 3 checks · default
- `verify_certificates` in `config.yaml` defaults to true. `pbi config set-verify-certificates true|false|PATH` sets it (`true`/`false` in any case; a path must name an existing file and is stored absolute, else exit 1 and nothing changes); setting false prints a warning. `pbi config get-verify-certificates` prints `Verify certificates: <value>`. config.verify-certificates · 7 checks
cache
- The cache folder holds `<key>/<version>/<key>.json`. A version is a directory under the key that contains `<key>.json`; versions are listed newest first (descending string order); a key is listed when it has at least one version, keys sorted; the latest data of a key is its first version. cache.layout · 8 checks
- A save creates one file `<key>/<version>/<key>.json` whose version is the local time as `YYYYMMDD_HHMMSS`, holding a JSON object with `cache_key`, `cached_at` (ISO 8601 time), `version`, `metadata` and `data`. cache.entry-format · 3 checks
- The cache key of `workspaces list` is `workspaces`, of `apps list` `apps_<role>`, of `users user-access` `user_access_<slug of the id>`; the request metadata saved is `{top, expand, filter}`, `{role}` and `{user_id}`. cache.keys · 16 checks
- `cache list [-k KEY]` and `cache clear [-k KEY] [-v VERSION]` print what is in the cache or report what was cleared, as in the examples; `clear` asks for confirmation (`--yes` skips; declining aborts with exit 1); `-v` without `-k` is refused with a message (exit 0); with no cache folder both say so. cache.list-and-clear · 15 checks
- `cache clear` removes everything in the folder (the folder stays), the key's whole folder, or the one version's file, and reports success even when nothing was there. cache.clear-effect · 0 checks
- A list command reads the cache only with `--use-cache` or `--cache-only`, and only when a cache folder is set and caching is enabled. cache.read-conditions · 7 checks
- A read takes the latest version of the command's key. cache.read-latest · 2 checks
- On a hit the first line is `Using cached data from <cached_at> (version: <version>)` with both fields taken from inside the cache file, not from the folder name. cache.notice-from-file · 3 checks · default
- If the latest version's file cannot be read as JSON the cache counts as unavailable; an older version is not used instead. cache.unreadable-latest · 2 checks · default
- With `--cache-only` and no usable cache, standard output says `Error: Cache not available and --cache-only was specified` (or, when no cache is set up or it is disabled, `Error: Cache not configured and --cache-only was specified` and a hint line), and the command exits 1 with `Aborted!`. cache.miss-cache-only · 6 checks
- With only `--use-cache` and an empty cache the command says `Cache not available, fetching from API...` (not when no cache is set up) and goes on to the API. cache.miss-use-cache · 3 checks
- Without a target folder the result is printed as a blank line, a rule of 80 `=`, the title (`Workspaces: n record(s)`, `Apps (<role>): n record(s)`, `User Access Information for: <id>`), a rule, the table (header line, one line per record) and a rule; with no records it prints `No workspaces found.` / `No apps (<role>) found.`. cache.table · 8 checks
- The workspaces table shows `id, name, type, state, isReadOnly, isOnDedicatedCapacity` and the apps table `id, name, description, publishedBy, lastUpdate`, in that order and only those that exist; when none exists all columns are shown, nested objects flattened into dotted columns. cache.table-columns · 5 checks · default
- With `--target-folder`, the list commands save `<name>.json` (the data on one line) and, with `-ft excel`, `<name>.xlsx`; `-n` sets the name (default `workspaces`, `apps`); the folder is created when missing and announced with `creating folder <path>`. cache.save-files · 3 checks
- A relative target folder is placed under `default_output_folder` when one is set, else under the current directory. cache.target-under-default-output · 2 checks
- `users user-access` with `--target-folder` is meant to save `<name>.json` / `.xlsx` like the other list commands; the original only prints and saves nothing. cache.user-access-save · 0 checks · open
- `workspaces list --use-cache|--cache-only -ft excel -tf ...`: what is saved as the workbook when the data came from the cache (the original crashes after writing the JSON). cache.workspaces-excel-from-cache · 0 checks · open
flatten
- Workspaces become seven sheets, in the order users, reports, dashboards, datasets, dataflows, workbooks, workspace: one row per child with the workspace's own (non-list) fields, in order, followed by the child's fields as `<child>_<field>`; sheet `workspace` has one row of own fields per workspace. Other list fields are dropped; sheets with no rows are kept. flatten.workspaces · 3 checks
- An app becomes the sheets users, reports, dashboards and app by the same rules. flatten.app · 3 checks
- Report users are flattened per workspace name: one row per user of each report, with the report's fields (without the users field) and the user's fields as `<key>_<field>` (key `reports_users_value` unless given); a report without users produces no row. flatten.report-users · 4 checks
- When a child's prefixed field has the same name as an own field, the child's value replaces it in place. flatten.child-wins · 1 check · default
- Each sheet is written as one worksheet of the same name, with a header row of the column names, no index column, and nested objects in a cell spread over dotted columns (`a.b`). (`apps list -ft excel` is an exception in the original: it has an index column; see REVIEW.) flatten.workbook · 0 checks
- Converting or saving as Excel a listing with no workspaces (an empty `value`, or no `value`): the original fails with an internal error. flatten.empty-listing · 0 checks · open
convert
- `workspaces format-convert -s S -t T` writes the workbook of the `flatten` sheets to T, whatever T is called, without needing a token; a missing or non-JSON S, or a T in a missing folder, fails. convert.workbook · 7 checks
- `apps augment` and `reports users` resolve the user group's token before anything else and write nothing without one. convert.apps-token · 3 checks
- A source with no apps (no `value`, or an empty list) needs no request: a json target holds `[]`. convert.apps-no-apps · 5 checks
- A missing source, or a source that is not a JSON object, fails. convert.apps-source · 5 checks
- With `-ft excel` the target is a folder without a file extension, created when missing; a target with an extension is refused and nothing is written (the original fails with an internal error where it means a usage error). convert.excel-target · 5 checks · default
- With apps in the source each app is fetched in full (`app.endpoint-app`); one the API answers with an error is reported in red (`Cannot download <app info>`) and skipped; a json target holds the list of fetched apps, an excel target gets `<app name>_<app id>.xlsx` per app (sheets of `flatten` kind app). convert.apps-fetch · 0 checks
- `reports users` also fetches the users of each report of each fetched app (`app.endpoint-report-users`) and replaces the report by the users answer's fields followed by the report's fields; a report whose fetch fails with an error answer is dropped from the app and logged; an excel target gets `<app name>_<app id>_report_users.xlsx`. convert.report-users · 0 checks
scan
- Each `workspace_ids` entry is a string (an id) or a mapping with a non-empty `id` and an optional `name`; entries keep their order, extra keys are ignored. scan.entry-shape · 5 checks
- An entry's label is `<name> (<id>)` or the id; its result file is `<slug of name>-<id>.json`, or `<id>.json` when there is no name or its slug is empty. The slug is ASCII (accents folded), lowercase letters and digits with every other run of characters as one `-`, no leading or trailing `-`. scan.label-and-file-name · 5 checks
- `workspace_ids` that is not a list, or an entry that is not a string or a mapping with an `id`, makes the config invalid. scan.entry-validation · 11 checks
- Config problems are reported one at a time, in this order: not a mapping; bad `workspace_ids`; no entries; no `target_folder`; then (after the folder is created) a non-boolean flag in the order lineage, datasource_details, dataset_schema, dataset_expressions, get_artifact_users; then non-numeric, then non-positive `interval`/`timeout`. Each is an `Error:` line and exit 1. scan.config-order · 14 checks
- A flag must be a YAML boolean; `1`, a string and null are refused with `'<flag>' must be a boolean value.` scan.flags-boolean · 4 checks
- `interval` (default 5) and `timeout` (default 300) are numbers or numeric strings; null or absent means the default; they must be above 0. scan.interval-timeout · 6 checks
- The config is read as YAML 1.1: `yes`, `no`, `on` and `off` are booleans. scan.yaml-1-1 · 1 check · default
- `target_folder` is resolved like other output folders (relative: under `default_output_folder` when set, else the current directory), created when missing and announced as `creating folder <path>` before the flags are checked. scan.target-folder · 4 checks
- Workspaces run one after another, each announced by a blank line and `=== Workspace <label> ===`; a failure prints `✗ <label>: <message>` and the loop goes on. scan.loop · 4 checks
- If any workspace failed, after the loop a blank line and `Failed workspaces: <labels, comma separated>` are printed and the exit code is 1; otherwise `✓ Scanned <n> workspace(s) into <path>` and exit 0. scan.exit-code · 4 checks
- A scanned workspace's result is saved as `<target folder>/<file stem>.json` (indent 2) and announced `✓ Saved <path>`. scan.result-file · 0 checks
- Each workspace gets its own scan: initiate with just its id, poll its status, fetch its result; a failure of one does not affect the others. scan.own-cycle · 0 checks
app
- Every request carries `Authorization: Bearer <token>`; GETs are plain, the one POST sends JSON. Failed connections and HTTP 500, 502 and 504 are retried up to 5 times with backoff factor 0.3. app.http · 0 checks · default
- Every API request verifies TLS certificates as the `verify_certificates` setting says (`config.verify-certificates`): true against the system CA bundle, a path against that CA bundle file, false not at all. Nothing else turns verification off. app.verify-certificates · 0 checks
- A JSON answer with an `error` key is a failure that shows the answer; HTTP errors of report, scan and export calls fail with their status. app.api-errors · 0 checks
- `workspaces list` calls `GET /admin/groups` with the query `$top=<top>` (default 1000), `$expand=<kinds joined by commas>` (default all of users, reports, dashboards, datasets, dataflows, workbooks) and, when given, `$filter=<odata filter>`; it prints `Retrieving workspaces for: ...` first. app.endpoint-workspaces-list · 0 checks
- `apps list` calls `GET /apps` for role user and `GET /admin/apps?$top=200` for role admin (admin token); it prints `Listing Apps as <role>` first. app.endpoint-apps-list · 0 checks
- `apps app` and `apps augment` get one app from `GET /apps/{appId}`, its reports from `GET /apps/{appId}/reports` and its dashboards from `GET /apps/{appId}/dashboards`, and return the app object with `reports` and `dashboards` lists (the `value` of those answers). app.endpoint-app · 0 checks
- `users user-access` calls `GET /admin/users/{userId}/artifactAccess` (admin token), takes `ArtifactAccessEntities` from the first page only (a continuation URI is not followed) and returns `{"artifacts": [...]}`. app.endpoint-user-access · 0 checks
- `reports list` calls `GET /groups/{groupId}/reports`; `reports pages` calls `GET /groups/{groupId}/reports/{reportId}/pages`, or for every report of the group (a report whose call fails is logged and skipped) a list of `{report_id, report_name, pages}`; `reports export` and `export` call `GET /groups/{groupId}/reports/{reportId}/Export` and save the bytes; the list and pages results are printed or saved as JSON with indent 2. app.endpoint-reports · 0 checks
- The users of a report come from `GET /admin/reports/{reportId}/users` (admin API). `workspaces report-users` reads the `reports` sheet of the Excel file `workspaces list` saved, keeps rows whose `type` is `Workspace` and (with `-wn`) whose `name` is listed, requests each row's `reports_id`, waits `-wi` seconds (default 3) after each success, merges the answer into the row as `reports_users_<field>` plus the row's own fields, logs failures and goes on, and saves `workspaces_reports_users.json` / `.xlsx` (json and excel by default) per workspace name, or prints a summary table without a target folder. app.endpoint-report-users · 0 checks
- A scan is `POST /admin/workspaces/getInfo?lineage=<bool>&datasourceDetails=<bool>&datasetSchema=<bool>&datasetExpressions=<bool>&getArtifactUsers=<bool>` with the JSON body `{"workspaces": [ids]}` (booleans as `true`/`false`, all five always sent), its state is `GET /admin/workspaces/scanStatus/{scanId}` and its result `GET /admin/workspaces/scanResult/{scanId}`. app.endpoint-scan · 0 checks
- `scan get` and each `scan batch` workspace poll the status every `interval` seconds (default 5, a positive number) until `Succeeded`, then fetch the result; `Failed` and running past `timeout` seconds (default 300) end the workspace with an error naming the scan id; a result that is HTTP 202 is not ready. app.scan-polling · 0 checks
- `scan get` takes one or more ids; `--target` and `--target-folder` exclude each other, and `--target-folder` needs exactly one id and saves `<id>.json` (indent 2); otherwise the result is printed as JSON. `scan initiate` prints the API answer, `scan status` the status answer, `scan result [-t FILE]` the result. app.scan-get-options · 0 checks
What it takes for granted
Facts about the world the pack relies on, and how to notice when one stops holding.
K-api-limits The admin endpoints are throttled by Power BI and the tool does not back off beyond a fixed retry on HTTP 500, 502 and 504; users keep to the limits with `--wait-interval`, scan `interval`/`timeout` and by scanning workspaces one by one.
Watch for: HTTP 429 answers.
K-api-shape The Power BI REST API answers with the shapes the tool reads: lists under `value`, failures under an `error` key, user access under `ArtifactAccessEntities`, scan ids under `id`, scan states `NotStarted`, `Running`, `Succeeded` or `Failed`, and HTTP 202 while a scan result is not ready.
Watch for: none
K-bearer-token The user supplies a valid Power BI bearer token (for the admin commands, one with admin rights on the tenant) and replaces it when it expires; the tool never obtains, refreshes or validates one.
Watch for: HTTP 401 and 403 answers from the API; the tool itself reports the failure.
K-keyring-optional A machine may have no OS keyring. Then tokens are kept in `~/.pbi_cli/credentials.json`; with a keyring they are kept there (service `pbi-cli`, user name = profile name) and the file is not used. The checks run without a keyring.
Watch for: `pbi profile list` shows ✓ for a profile only if its token is found in either place.
K-single-user One `pbi` process at a time uses a home directory and a cache folder, and the machine's clock does not go backwards between runs.
Watch for: none
powerbi-cli (pbi)
pbi is a command line tool for people who administer or document a Power BI tenant. They give it a bearer token, and it asks the Power BI REST API (https://api.powerbi.com/v1.0/myorg) for workspaces, apps, reports, report users, user access and workspace scans, and saves the answers as JSON files and Excel workbooks, one folder per day if they like. Results can be cached in a folder (local or S3) so later runs can work offline, and saved files can be fed back into later commands (convert to Excel, fetch the users of each report).
Its users script it (PowerShell or bash loops, scheduled backups), so what they rely on is the command line, the exit codes, the names and shapes of the files it writes, the profile/config/cache files in ~/.pbi_cli, and the messages that say why a run failed. Almost every command needs the live API and cannot be checked here; the pack pins what can be checked offline (profiles and settings, the cache, the saved-file converters, the scan config) and records the API calls as decisions.
Non-goals
- Getting a token. The user brings a bearer token; there is no sign-in flow, no refresh, no service principal login.
- Changing anything in Power BI. Every call is a read (the one POST asks for a scan).
- A graphical interface, a server, or a library API for others to import.
- Other clouds (national or sovereign Power BI endpoints) and API versions other than
v1.0/myorg. - Concurrency control: two
pbiprocesses sharing one home directory are not supported.
Priorities
When goals conflict, earlier ones win:
- Scripts keep working: command names, options, exit codes, file names and the shapes of saved JSON and Excel files do not change.
- Tokens stay out of sight: kept in the OS keyring when there is one, otherwise in a file only the user can read; never printed or logged.
- A run fails loudly and says why; one failing workspace or app does not stop a batch that can go on.
- Work offline from the cache and from saved files whenever the data is already there.
- Few dependencies, plain files, no hidden state beyond
~/.pbi_cliand the cache folder.
Who resolves gaps
emptymalei, as the namespace holder. If the pack doesn't settle something, record the question and the reading you chose in NOTES.md next to your build; obligations marked open in obligations.toml are yours to choose.
powerbi-cli: the product
pbi is one command line program that reads Power BI's REST API and saves the answers as files. The components in this pack are its core (the parts that can be checked without the API); this page says what the whole program is.
Shape
- A command line program,
pbi, written in Python (3.10 or later) and installed as a package namedpbi_cli(uv sync, thenuv run pbi, orpip install). It runs on Windows, macOS and Linux; its documentation shows PowerShell and bash loops around it. No server, no daemon, no GUI. - Command line parsing with
click, nested command groups with--helpon each, short options like-t,-tf,-ft,-wn,-wias listed below. Run with no command it greets and points to--help;pbi versionprints the installed package version. - Tables and Excel files with
pandasandopenpyxl; configuration and the scan config with YAML; HTTP withrequests; file names withpython-slugify; cache locations throughcloudpathlib(local folders ands3://); the OS keyring throughkeyringwhen one exists. Logging goes to standard error at INFO level; results and messages go to standard output. - It needs a Power BI bearer token supplied by the user (see Interfaces). Commands marked admin need a token with admin rights.
- State lives in
~/.pbi_cli/(config.yaml,credentials.json, the legacyauth.jsonandprofiles.json) and in the optional cache folder. Nothing else is kept between runs.
Surfaces
Setup and local state (offline, checked):
pbi auth -t TOKEN [-p PROFILE] [-g user|admin]: store a token as a profile. (auth-profiles)pbi profile list | switch [NAME] [-g G] | delete NAME [-g G] [--yes]: manage profiles. (auth-profiles)pbi config set-output-folder P | get-output-folder | set-cache-folder P | get-cache-folder | enable-cache | disable-cache | set-verify-certificates true|false|PATH | show: settings. (config-cache)pbi cache list [-k KEY] | clear [-k KEY] [-v VERSION] [--yes]: look at and empty the cache folder. (config-cache,cache-keys)pbi version, andpbialone (a greeting).
Reading the API (needs a token and the network; each can also read the cache, see Flows):
pbi workspaces list [--top N] [-e KIND]... [-f ODATA] [-ft json|excel]... [-tf FOLDER] [-n NAME] [--use-cache|--cache-only]: admin; all workspaces, with users, reports, dashboards, datasets, dataflows, workbooks expanded by default; prints a table, or savesNAME.jsonandNAME.xlsx(default nameworkspaces).pbi apps list [-r user|admin] [-tf FOLDER] [-ft ...] [-n NAME] [--use-cache|--cache-only]: the apps the user can see, or all apps of the tenant (admin); default file nameapps.pbi apps app -a APP_ID [-t FILE] [-ft json|excel]: one app with its reports and dashboards; prints JSON, or saves it (an Excel file has the sheets offlattenkind app).pbi users user-access -u USER_ID [-tf FOLDER] [-ft ...] [-n NAME] [--use-cache|--cache-only]: admin; the artifacts a user can access, as{"artifacts": [...]}.pbi reports list -g GROUP_ID [-t FILE],pbi reports pages -g GROUP_ID [-r REPORT_ID] [-t FILE],pbi reports export -g GROUP_ID -r REPORT_ID [-t FILE]and the olderpbi export -g GROUP_ID -r REPORT_ID [-t FILE]: reports of a workspace, their pages (all reports of the group when-ris left out; a report whose pages cannot be fetched is skipped and logged), and the exported report file (without-tit prints the size instead of the content).pbi workspaces scan initiate IDS... | status SCAN_ID | result SCAN_ID [-t FILE] | get IDS... [-t FILE | -tf FOLDER]: admin; the workspace-info scan, step by step or all at once, with--lineage --datasource-details --dataset-schema --dataset-expressions --get-artifact-users,--interval(default 5) and--timeout(default 300) forget.-tand-tfexclude each other and-tftakes exactly one workspace id (it saves<id>.json).pbi workspaces scan batch -c CONFIG.yaml: admin; scans many workspaces one by one from a config file. (scan-batch,workspace-entries)
Working on saved files:
pbi workspaces format-convert -s WORKSPACES.json -t WORKSPACES.xlsx: JSON to Excel. (file-converters,flatten)pbi apps augment -s APPS.json -t OUT [-ft json|excel]: fetch every listed app in full. (file-converters,flatten)pbi reports users -s APPS.json -t OUT [-ft json|excel]: the same, plus the users of each report. (file-converters,flatten)pbi workspaces report-users -s WORKSPACES.xlsx [-wn NAME]... [-wi SECONDS] [-ft json|excel]... [-t FOLDER] [-n NAME]: admin; reads thereportssheet of the Excel fileworkspaces listsaved, keeps rows whosetypeisWorkspace(and, with-wn, whose workspacenameis listed), fetches the users of each report (reports_idcolumn), waiting-wiseconds (default 3) between requests, and savesworkspaces_reports_users.json/.xlsx(name with-n) with one list of enriched report records per workspace name, flattened byflattenkind report_users; prints a summary table without-t.
Files users touch: ~/.pbi_cli/config.yaml, ~/.pbi_cli/credentials.json, the cache folder (<key>/<version>/<key>.json), saved *.json and *.xlsx results, scan config YAML files (an example is examples/scan_config.example.yaml).
Flows
- First setup.
pbi auth -t TOKEN -p work -g admin(and-g userfor a user token),pbi profile listshows ✓ marks and the active profile of each group,pbi profile switch work -g adminchanges it. Commands then use the right group's active profile. (auth-profiles) - Back up all workspaces.
pbi config set-output-folder ~/PowerBI/backups, thenpbi workspaces list -ft json -ft excel -tf 2024-01-01fetches (admin API), caches the result if a cache folder is set (cache-keys), and savesworkspaces.jsonand a workbook with the sheets users, reports, dashboards, datasets, dataflows, workbooks and workspace (flatten) in~/PowerBI/backups/2024-01-01/. A JSON saved earlier is converted later withpbi workspaces format-convert. (config-cache,file-converters) - Work offline from the cache.
pbi config set-cache-folder ~/PowerBI/cacheonce; every list command saves what it fetched as a new version.pbi workspaces list --cache-onlyprints the latest saved result without a token or network,--use-cachefalls back to the API when nothing is cached;pbi cache list [-k workspaces]shows keys and versions,pbi cache clear -k workspaces -v VERSIONremoves some. (cached-reads,config-cache,cache-keys) - Who can open which report.
pbi workspaces list -ft excel -tf ..., thenpbi workspaces report-users -s workspaces.xlsx -wn "Finance" -wi 5 -tf ...saves the report users of one workspace; orpbi apps list -tf ...,pbi reports users -s apps.json -t out.jsonfor apps. (file-converters,flatten) - Scan many workspaces. Copy
scan_config.example.yaml, list workspace ids (with optional names for readable file names), settarget_folder, runpbi workspaces scan batch -c scan.yaml: each workspace is scanned on its own (start the scan, poll its status everyintervalseconds up totimeout, fetch the result) and saved as<name-slug>-<id>.json; failed workspaces are listed at the end and the exit code is 1. (scan-batch,workspace-entries) - Look at one thing.
pbi apps app -a ID,pbi reports list -g GROUP,pbi reports pages -g GROUP -r REPORT,pbi users user-access -u USER,pbi reports export -g GROUP -r REPORT -t report.pbix,pbi workspaces scan get IDS... --lineage -t result.json.
Interfaces
- Power BI REST API,
https://api.powerbi.com/v1.0/myorg(hard-coded), with the headerAuthorization: Bearer <token>on every call. TLS certificates are verified unless theverify_certificatessetting says otherwise (app.verify-certificates). Failed connections and HTTP 500, 502 and 504 are retried up to 5 times with a 0.3 backoff factor. Which endpoint and query parameters each command uses is in theapp.*obligations. - Credentials: bearer tokens per profile name, in the OS keyring (service
pbi-cli) when it works, otherwise in~/.pbi_cli/credentials.json(mode 600). Profiles are ungrouped or in the groupsuserandadmin. - Cache storage: any folder
cloudpathlibunderstands; local folders ands3://are documented (credentials for S3 come from the standard AWS configuration),gs://andaz://are accepted by the setting.
Order
Build the state layer first (auth-profiles, config-cache, the cache layout of cache-keys), then flatten and workspace-entries, then the saved-file commands and cached-reads, then the API commands one by one (workspaces list, apps, users, reports) and last the scans (scan-batch, scan get). A build that has the offline commands and the cache already serves scripted users who work from saved files.