# Power BI export

> Connect Power BI to an organization's results with a read-only API key and the Power Query snippets in Settings, and see what each dataset contains.

- Plans: Enterprise
- Canonical: https://docs.devlin.ai/integrations/power-bi-export

The Power BI export is a read-only data feed that Power BI pulls from on each refresh. It serves these datasets as JSON: totals per simulation, totals per coach, daily activity, one row per learner attempt, and one row per transcript message. An owner or admin of an organization workspace on the Enterprise plan sets it up by creating an API key and pasting a ready-made Power Query snippet into Power BI.

## Before you start
- The organization's plan must be Enterprise. devlin.ai can also turn the export on for an individual organization. On any other plan the panel shows a **Power BI export** card with a **View plans** button in place of the controls.
- You must be an owner or admin of the organization. Members cannot open the organization's **Integrations** section. See [Members, roles, invitations and the activity log](https://docs.devlin.ai/workspace-and-team/members-and-roles).
- The controls are in an organization's settings only. The **Integrations** page of a personal workspace has no Power BI panel. See [Personal and organization workspaces](https://docs.devlin.ai/workspace-and-team/workspaces).
- A key gives whoever holds it read access to the organization's results, including what learners wrote and any learner identity that was collected. Treat it like a password.
- You need Power BI Desktop to paste the snippet into, and the Power BI service if you want scheduled refresh.

## Steps
Create a key:

1. Make the organization your active workspace (**Settings → Workspaces** lists your workspaces and switches to the one you pick), then open **Integrations** in the settings sidebar.
2. Under **API keys**, type a name for the key that says what will use it. The name can be up to 100 characters.
3. Select **Create key**.
4. A banner shows the full key with a copy button. Copy it and store it somewhere safe before you dismiss the banner. The key is shown this one time only. devlin.ai does not keep a readable copy, so it cannot be shown again or recovered.

An organization can have up to 5 active keys. When that many exist, creating another fails with "Limit of 5 active keys reached. Revoke one first."

Connect Power BI, following the instructions under **Connect Power BI** in the same panel:

1. Under **Connect Power BI**, select a dataset: `simulations`, `coaches`, `activity`, `attempts` or `transcripts`. The snippet below the buttons changes to match. Copy it with the copy button on the snippet.
2. In Power BI Desktop, the panel directs you to Get Data, then Blank Query, then Advanced Editor. Paste the snippet there.
3. In the snippet, replace `YOUR_API_KEY` with the key you copied.
4. If Power BI asks how to connect, choose **Anonymous**. The key inside the query is what authenticates the request.
5. Optional: the snippet's `StartDate` line sets the earliest data to pull and starts at 2020-01-01. Set a later date to pull less history.
6. Repeat for each dataset you want, one query per dataset.
7. Schedule refresh in the Power BI service.

Revoke a key:

1. In the **API keys** list, find the key. Each entry shows the name, the first characters of the key (keys start with `dvlk_`), the date it was created, and the date it was last used or "never used".
2. Select **Revoke**, then confirm with **Revoke key** in the **Revoke this key?** dialog.

Creating and revoking a key are both recorded in the organization's activity log with the key's name. See [Members, roles, invitations and the activity log](https://docs.devlin.ai/workspace-and-team/members-and-roles).

A revoked key stops working immediately and leaves the list. Keys do not expire, and a key cannot be edited or regenerated. A key belongs to the organization, not to the person who created it: it keeps working after that person leaves the organization or changes role, until an owner or admin revokes it. To rotate a key, create a new one, update the queries in Power BI, then revoke the old one.

## Result
Each query loads one table. The snippet requests the dataset page by page until the feed reports no further page, then combines the pages. When a dataset has no rows for the range, the query still returns an empty table with the dataset's columns, so later steps in your model keep working.

Every refresh pulls the whole range again, from `StartDate` to the moment of the refresh.

The feed is read-only. Nothing in Power BI can change the workspace.

## What the datasets leave out
Session counts, attempt rows and transcript rows leave out:

- Designer test runs.
- Sessions where the learner never sent a message.
- The replaced part of a voice session that dropped and was resumed. The resumed session is the one that counts.

Evaluation counts and scores leave out evaluations that belong to a designer test run or to the replaced part of a resumed voice session.

The `credits` columns of `simulations`, `coaches` and `activity` are counted from the workspace's usage charges, not from the sessions, so they are not reduced by these rules.

No dataset contains evaluator submissions or results. To get them, see [Exporting results, transcripts and evaluations](https://docs.devlin.ai/results-and-analytics/exports).

The `attempts` and `transcripts` datasets cover simulations only. Coaches appear as totals in `coaches` and as a daily count in `activity`.

## Datasets and columns
Times are ISO timestamps. A column described as empty holds a null value.

### simulations
One row per simulation in the workspace, including simulations with no activity in the range. Counts cover the range the query asks for.

| Column | Meaning |
|---|---|
| `simulation_id` | The simulation's ID. Join to `attempts` and `transcripts` on this column. |
| `name` | The simulation's name. |
| `status` | The simulation's status. |
| `sessions` | Learner sessions that started in the range. |
| `completions` | Those sessions that have ended. |
| `unique_learners` | Distinct learners among those sessions, counted by learner ID, or by browser session where there is no learner ID. |
| `evals` | Evaluations created in the range. This follows the evaluation's time, not the session's start time. |
| `pass_rate` | The share of scored evaluations that passed, as a whole percent. Empty when no evaluation in the range was scored. Feedback-only evaluations are not counted as scored. |
| `avg_score` | The average score percent of scored evaluations. Empty when none was scored. |
| `credits` | Credits charged to the simulation in the range. |
| `last_activity` | When the most recent counted session in the range started. Empty when there was none. |

### coaches
One row per coach in the workspace, including coaches with no activity in the range.

| Column | Meaning |
|---|---|
| `coach_id` | The coach's ID. |
| `name` | The coach's name. |
| `status` | The coach's status. |
| `sessions` | Coach sessions that started in the range. |
| `unique_learners` | Distinct learners among those sessions, counted the same way as for simulations. |
| `credits` | Credits charged to the coach in the range. |
| `last_activity` | When the most recent counted session in the range started. Empty when there was none. |

### activity
One row per day for the whole workspace. A day with no sessions, evaluations or credit usage has no row. This dataset comes back in a single response.

| Column | Meaning |
|---|---|
| `bucket_start` | The start of the day. |
| `sessions` | Simulation sessions that started that day. |
| `completions` | Those sessions that have ended. |
| `coach_sessions` | Coach sessions that started that day. |
| `evals` | Evaluations created that day. |
| `pass_count` | Evaluations created that day that passed. |
| `avg_score` | The average score percent of that day's scored evaluations. Empty when none was scored. |
| `credits` | Credits the workspace used that day, counted from its usage charges, not only learner sessions. |

### attempts
One row per learner attempt at a simulation, oldest first. The range applies to when the attempt started.

| Column | Meaning |
|---|---|
| `conversation_id` | The attempt's ID. Join to `transcripts` on this column. |
| `simulation_id` | The simulation's ID. |
| `simulation_name` | The simulation's name. |
| `session_id` | The learner's browser session. |
| `learner_id` | The learner ID stored with the attempt. |
| `learner_identifier` | The identifier the learner entered or the course passed in. Empty if none was collected, or after the workspace's retention window cleared it. |
| `learner_attributes` | The attempt's learner identity field values as one piece of JSON text. Empty if none were collected, or after the workspace's retention window cleared them. Parse it in Power Query to get one column per field. |
| `started_at` | When the attempt started. |
| `ended_at` | When the attempt ended. Empty if it has not ended. |
| `mode` | `text` or `voice`. |
| `completed` | True when the attempt has ended. |
| `credits` | Credits charged to the attempt. |
| `eval_pass` | Whether the attempt's most recent evaluation passed. Empty when there is no evaluation or it was feedback-only. |
| `eval_score` | The score percent of the most recent evaluation, rounded to a whole number. Empty when there is no evaluation or it was feedback-only. |

`eval_pass` and `eval_score` are the AI's result. A reviewer's adjustment from human review is not applied.

The organization's learner identity visibility setting does not hide the learner identity columns in this feed. In a workspace with a retention window, `learner_id`, `learner_identifier` and `learner_attributes` are emptied on attempts older than the window. Only owners and admins can create a key, and they always see learner identity. See [Collecting learner identity](https://docs.devlin.ai/workspace-and-team/learner-identity).

### transcripts
One row per stored message in a simulation attempt, oldest first. The range applies to when the message was sent. To get the learner, the simulation's name, the mode or the evaluation for a message, join to `attempts` on `conversation_id`.

| Column | Meaning |
|---|---|
| `message_id` | The message's ID. |
| `conversation_id` | The attempt the message belongs to. |
| `simulation_id` | The simulation's ID. |
| `role` | `user` for the learner, `assistant` for the character. |
| `content` | The text of the message. |
| `created_at` | When the message was sent. |

A workspace's retention window deletes old messages, so they stop appearing in `transcripts` while the attempt can still be counted elsewhere. See [Data privacy and retention](https://docs.devlin.ai/workspace-and-team/data-privacy-and-retention).

## Date range
Each request takes an optional `from` and `to`, both ISO dates. The snippet always sends `from` with the value of `StartDate` and leaves `to` out, which means up to now. A request with no `from` gets the 365 days before `to`, so if you write your own query, send `from` or older rows will drop out of your model as they age past that window.

## Limits
- Each key can make 60 requests per minute. A request above that gets `Too many requests` with a `Retry-After` header that says how many seconds to wait. Each page of a dataset is one request.
- The feed sets no row limit of its own. `simulations`, `coaches`, `attempts` and `transcripts` arrive as more pages, and `activity` arrives in one response.

## If the plan changes
The feed checks the organization's plan on every request. When the organization is no longer on a plan that includes the export, requests are refused straight away and scheduled refreshes fail. The keys are not revoked. The panel lists them under **Existing API keys** so you can revoke them, and you cannot create new ones. If the organization returns to a plan that includes the export, keys that were not revoked work again.

## Errors a refresh can show
The feed answers a failed request with a short JSON error, which Power BI reports as a failed web request.

| Status and error | What it means |
|---|---|
| `401` `Unauthorized` | The request has no key, the key is mistyped, or it is not a key this feed knows. Check that `YOUR_API_KEY` was replaced with the whole key. |
| `403` `Unauthorized` | The key was revoked, or the organization's plan no longer includes the export. |
| `429` `Too many requests` | The key passed its per-minute limit, or too many requests with a bad key came from the same network address. Wait and refresh again. |
| `400` `Unknown dataset` | The dataset name in the URL is not one of the datasets listed above. |
| `400` `from must be before to` | `StartDate` (or a `from` you added) is later than `to`, or later than now when `to` is left out. |
| `400` `from and to must be valid ISO dates` | `StartDate` (or a `from` or `to` you added) is not a valid date. |
| `400` `Invalid cursor` | The page marker was altered. Use the snippet as copied. |
| `500` `Internal server error` | The request failed on the devlin.ai side. Refresh again. |

In the settings panel, "Could not load Power BI settings. Refresh to try again." means the key list did not load. Reload the page.

## Related
- [Exporting results, transcripts and evaluations](https://docs.devlin.ai/results-and-analytics/exports)
- [The Results dashboard](https://docs.devlin.ai/results-and-analytics/results-dashboard)
- [Collecting learner identity](https://docs.devlin.ai/workspace-and-team/learner-identity)
- [Data privacy and retention](https://docs.devlin.ai/workspace-and-team/data-privacy-and-retention)
- [Members, roles, invitations and the activity log](https://docs.devlin.ai/workspace-and-team/members-and-roles)
- [Plans and what each includes](https://docs.devlin.ai/plans-and-billing/plans-and-features)
- [MCP server and AI tool connectors](https://docs.devlin.ai/integrations/mcp-server-and-ai-tool-connectors)
