> For the complete documentation index, see [llms.txt](https://portavi.gitbook.io/portavi-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://portavi.gitbook.io/portavi-docs/developers/power-bi.md).

# Power BI and Excel

Portavi publishes an **OData feed**, the format Power BI and Excel read natively. You give them one address and a token; they find the tables, name and type the columns, and page through every row by themselves. You don't need a connector to install or any Power Query code to write.

The feed reads the same data as [the Portavi API](/portavi-docs/developers/rest-api.md), with the same tokens and the same rules about who sees what. Like the API, it is read-only and included on **Scale** and above.

## Connecting Power BI Desktop

1. Create an access token under **Settings → Security → API access**. [Connecting an AI client](/portavi-docs/ai/connecting-ai-clients.md#creating-an-access-token) has the steps. Give it a name such as "Power BI" so you can find it later.
2. In Power BI Desktop, choose **Get data → OData feed**.
3. Enter the address and choose **OK**:

```
https://app.portavipmo.com/odata/v1
```

4. When asked how to sign in, choose **Basic**. Type `token` as the user name (any user name works) and paste your access token as the password. Then choose **Connect**.
5. Tick the tables you want, for example **Projects**, **Tasks** and **Risks**, and choose **Load** or **Transform data**.

## Scheduled refresh in the Power BI service

After you publish the report, open the semantic model's settings in the Power BI service. Under **Data source credentials**, edit the OData source: choose **Basic**, enter the same user name and token, and set the privacy level to **Organizational**. The feed is on the internet, so the model doesn't need a gateway. Then turn on **Scheduled refresh**.

When the token expires, refresh stops with a sign-in error. Create a new token and enter it under **Data source credentials**.

## Excel

In Excel for Windows or Mac, choose **Data → Get Data → From Other Sources → From OData Feed**, enter the same address, and sign in with **Basic** in the same way.

## The tables

| Table                                     | Holds                                           |
| ----------------------------------------- | ----------------------------------------------- |
| Portfolios, Programmes, Projects          | the hierarchy, with health and budget           |
| HealthHistory                             | every health and status change, for trend lines |
| Tasks, Milestones, Phases, Sprints        | delivery                                        |
| FlaggedItems                              | tasks flagged for attention                     |
| Risks, Issues, Assumptions, Dependencies  | the RAID log                                    |
| Decisions, Lessons, ChangeRequests        | governance                                      |
| Benefits, BenefitMeasurements, Objectives | benefits and strategy                           |
| BudgetEntries                             | actual spend, one row per cost                  |
| Members                                   | names and roles                                 |

The columns are the fields in the [API reference](https://app.portavipmo.com/api/v1/openapi.json). Join tables on their ids: `Tasks.project_id` to `Projects.id`, `Projects.programme_id` to `Programmes.id`, and `owner_id` or `assignee_id` to `Members.id`.

A `budget` column holds a record; expand it in Power Query to get the total, the currency and the reserves. A `labels` column holds a list.

When a feature is switched off in the workspace, its table is still there but empty, so a report built while it was on keeps refreshing.

## Large workspaces and incremental refresh

Power BI reads a table up to 500 rows at a time, following the feed's next-page links until it reaches the end. Filters you apply in Power Query on dates, ids and statuses are sent to Portavi, so only the matching rows travel.

For big tables such as **Tasks**, set up [incremental refresh](https://learn.microsoft.com/power-bi/connect-data/incremental-refresh-overview) on `updated_at`: filter the column with the `RangeStart` and `RangeEnd` parameters, and each refresh then reads only the rows that changed in that window.

Incremental refresh never sees deleted items, because they leave no row behind. Schedule an occasional full refresh to drop them. [Paging and incremental loads](/portavi-docs/developers/paging-and-incremental-loads.md#loading-only-what-changed) explains why.

## What the feed doesn't do

The feed supports filtering, choosing columns and taking the first rows. It refuses sorting, skipping rows, counting and expanding related tables, rather than ignore them and return the wrong data. If a refresh fails with "not supported by this feed", move that step after the data has loaded: sort in the report, or merge tables in the model instead of in the query.

## For developers

Any OData v4 client can read the feed. It serves the service document at the root and the schema at `/odata/v1/$metadata`. Sign in with Basic as above, or send `Authorization: Bearer <token>`.

| Option                                                        | Supported                                                                                                                                                 |
| ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `$filter`                                                     | `eq`, `ne`, `gt`, `ge`, `lt`, `le`, `and`, `or`, `not`, `in`, `contains`, `startswith`, `endswith`, `tolower`, `toupper`; record fields as `budget/total` |
| `$select`                                                     | yes; `id` always comes back                                                                                                                               |
| `$top`                                                        | yes                                                                                                                                                       |
| Paging                                                        | `@odata.nextLink`; ask for smaller pages with `Prefer: odata.maxpagesize=<n>`                                                                             |
| A single row                                                  | `Projects(<id>)`                                                                                                                                          |
| `$orderby`, `$skip`, `$count`, `$expand`, `$search`, `$apply` | refused with `501`                                                                                                                                        |

Rows come oldest change first. Errors use the OData error format, `{"error": {"code": "…", "message": "…"}}`, with the codes listed in [Errors and rate limits](/portavi-docs/developers/errors-and-rate-limits.md). The rate limits are the API's. Each response carries the `Portavi-Rows-Omitted` header, as the API's lists do.
