Claude Code (Part 16)

Neil Haddley โ€ข October 17, 2026

Installing Microsoft's official Dataverse plugin from the Claude marketplace, connecting it to a Dataverse environment, and reading and writing records in plain English

AIPower Platformclaude-codedataverseclaude-pluginspac-clidataverse-cli

In Part 13 I built my own Claude Code skill, dv-generate, which writes Dataverse solution XML without touching a live environment. Microsoft has since published an official Dataverse plugin on the Claude marketplace. It works the other way round: it connects Claude Code to a live environment, and then reads and writes tables and records there.

The plugin is open source (MIT), and Microsoft describes it in Dataverse Plugin Is Now on the Claude Marketplace.

The Microsoft Dataverse plugin on the Claude marketplace, made by Microsoft, with 2,707 installs when I looked

The Microsoft Dataverse plugin on the Claude marketplace, made by Microsoft, with 2,707 installs when I looked

Installing the plugin

I opened Claude Code in an empty folder and installed the plugin from the official marketplace.

PROMPT
1/plugin install dataverse@claude-plugins-official
I typed the install command, and Claude Code showed the plugin's description

I typed the install command, and Claude Code showed the plugin's description

The plugin details listed the nine skills it would install, and its context cost: about 1.1k tokens on every turn, and about 51.1k when a skill is used. I chose to install it for this folder only (local scope).

The plugin details listed the nine skills it would install, and its context cost: about 1.1k tokens on every turn, and about 51.1k when a skill is used. I chose to install it for this folder only (local scope).

The plugin installed and became active straight away, and I typed /skills to check

The plugin installed and became active straight away, and I typed /skills to check

/skills listed the nine dataverse: skills, locked by the plugin, next to my own dv-generate skill from Part 13

/skills listed the nine dataverse: skills, locked by the plugin, next to my own dv-generate skill from Part 13

The Dataverse-skills repository lists the nine skills, in this order:

SkillWhat it does
dv-connectOne-time setup: installs the Dataverse CLI, the Python SDK and the PAC CLI, signs in to the environment, and registers the Dataverse MCP server
dv-queryReads, filters, pages through and aggregates records, answers questions in plain English, and loads results into pandas DataFrames
dv-dataCreates, updates and deletes single records, and bulk imports: CSV files, several tables with foreign keys, upserts by alternate key, and AI-generated sample data
dv-metadataAuthors and edits the data model: tables, columns, relationships, forms and views
dv-solutionManages the solution lifecycle: create, export, import, promote across environments, and validate deployments
erp-xppThe Finance and Operations X++ lifecycle: authoring, SDK installation, compiling, deploying, database sync and verification (Windows only)
dv-adminEnvironment administration: bulk delete, retention and archiving, organisation and OrgDB settings, the recycle bin, audit, and Power Platform admin center toggles
dv-securityAssigns security roles, manages user access, adds application users, configures business units, and elevates an admin
dv-overviewThe cross-cutting rules and tool routing, loaded before any other skill so that each request goes to the right specialist

The same skills install into other coding agents too. The README gives install commands for GitHub Copilot, Gemini CLI, Codex CLI, Cursor and Google Antigravity, as well as Claude Code.

The plugin chooses the tool for each job. The Dataverse MCP server handles simple reads and small writes, the Python SDK handles bulk work, the Dataverse CLI handles data commands, and the PAC CLI handles solutions and tenant operations.

Connecting to an environment

PROMPT
1connect to my Dataverse Environment
I asked Claude to connect to my Dataverse environment

I asked Claude to connect to my Dataverse environment

The dv-connect skill first looked for existing PAC CLI sign-ins on my Mac and asked which environment this folder should use. I typed the address of a development environment that was not in the list, so Claude created a new Dataverse CLI sign-in for it, using a device code.

Claude ran dataverse auth create with --deviceCode and gave me a code to enter

Claude ran dataverse auth create with --deviceCode and gave me a code to enter

I opened login.microsoft.com/device and entered the code

I opened login.microsoft.com/device and entered the code

Microsoft asked me to consent to the Dataverse CLI accessing Dataverse MCP tools and reading my profile

Microsoft asked me to consent to the Dataverse CLI accessing Dataverse MCP tools and reading my profile

After I accepted, the browser said I had signed in to the Dataverse CLI and could close the window

After I accepted, the browser said I had signed in to the Dataverse CLI and could close the window

Claude checked the new profile with dataverse auth who, and then started a second device-code sign-in, this time for the PAC CLI on the same environment

Claude checked the new profile with dataverse auth who, and then started a second device-code sign-in, this time for the PAC CLI on the same environment

Asking about the environment

The PAC CLI sign-in was still waiting for me, but I asked a question anyway.

PROMPT
1What tables are in this environment?
Claude said it could already answer with the Dataverse CLI, without waiting for the rest of the setup, and called the Web API for the table definitions

Claude said it could already answer with the Dataverse CLI, without waiting for the rest of the setup, and called the Web API for the table definitions

The environment had 701 tables. Claude grouped them: 213 standard tables, 488 tables flagged as custom that are almost all Microsoft's own (Dynamics 365, Power Pages and Copilot), and one table that someone built, hadd_todo

The environment had 701 tables. Claude grouped them: 213 standard tables, 488 tables flagged as custom that are almost all Microsoft's own (Dynamics 365, Power Pages and Copilot), and one table that someone built, hadd_todo

I liked that Claude did not simply report 489 custom tables. It worked out that the msdyn_, mspp_ and adx_ tables come from Microsoft, and picked out the one table with a real publisher prefix.

Writing a record

PROMPT
1Create an account named Contoso
Claude read the help for dataverse data and dataverse data create before running anything

Claude read the help for dataverse data and dataverse data create before running anything

Claude said which environment it was about to write to, created the account, and reported its ID, owner and status

Claude said which environment it was about to write to, created the account, and reported its ID, owner and status

Reading records

PROMPT
1list todo items
Claude described the hadd_todo table first. Its first try passed an --environment option that describe does not accept, so it ran the command again without it.

Claude described the hadd_todo table first. Its first try passed an --environment option that describe does not accept, so it ran the command again without it.

Claude queried the table and showed the two Todo items as a table, with each record's title, status, priority, due date and whether it was completed

Claude queried the table and showed the two Todo items as a table, with each record's title, status, priority, due date and whether it was completed

The nine skills, with examples

I never named a skill in my session. I asked in plain English, and Claude chose the skill whose description matched. To see each skill at work, I went back to the same folder and typed one or two prompts for each skill into a single Claude Code session, one after another, against my development environment. Under each prompt is what Claude Code showed: the skill it loaded, the commands it ran (shortened), and its reply.

dv-overview: the rules the others follow. It loads first for any Dataverse, Dynamics 365 or Power Platform request. It does no work itself. It holds the rules: check what is already connected before setting anything up, use Python rather than Node.js for scripts, confirm the environment before any change, and always ask for the publisher prefix, because a prefix can never be changed. It also says what is out of scope: canvas apps, Power Automate flows and Business Central. There is no prompt for it, but it shows up in the first transcript below.

dv-connect: one-step setup. It checks the tools, chooses the environment, signs in to the Dataverse CLI and the PAC CLI, writes a .env file, and registers the MCP server. Every step checks whether it is already done and skips it if so.

PROMPT
1Connect to my Dataverse environment

This was the first prompt of my session, and the screenshots above show what happened: a choice of environment, two device-code sign-ins, and the consent screen.

dv-query: reading and analysing records. Filtered reads, counts, SQL, FetchXML for joins and totals, and pandas DataFrames. If dataverse auth who shows an active sign-in, it skips the setup and queries straight away.

PROMPT
1How many accounts are there in my Dataverse environment?
TEXT
1โ— I'll load the Dataverse overview skill first, then use the query skill to count accounts.
2โ— Skill(dataverse:dv-overview)
3โ— Skill(dataverse:dv-query)
4โ— ToolSearch(dataverse)
5  โŽฟ No matching deferred tools found
6โ— Bash(dataverse auth who)
7  โŽฟ User:          admin@p8lf.onmicrosoft.com
8    Environment:   https://org62b9d1ca.crm.dynamics.com/
9โ— No MCP tools found, but there's an active CLI profile. I'll use the Dataverse CLI directly to count accounts.
10โ— Bash(dataverse data count --table accounts)
11  โŽฟ Count: 1
12โ— There is 1 account in your Dataverse environment (https://org62b9d1ca.crm.dynamics.com/, org dev-no-721df8ab).
13
14  Note: Dataverse MCP tools aren't set up in this session โ€” I used the Dataverse CLI instead. If you'd like
15  conversational querying going forward, I can set up MCP.

The overview's first rule is visible here: Claude looked for MCP tools, found none, found the CLI sign-in, and used that.

PROMPT
1Total the open Todo items by priority
TEXT
1โ— Bash(dataverse api request --path "/api/data/v9.2/EntityDefinitions?...")
2  โŽฟ ERROR: Absolute URL host '' does not match the environment host 'org62b9d1ca.crm.dynamics.com'.
3โ— Bash(dataverse api request --path "EntityDefinitions?...")
4  โŽฟ HTTP Error 404 - Not Found
5โ— Bash(dataverse api request --path "api/data/v9.2/EntityDefinitions?$select=LogicalName,EntitySetName")
6  โŽฟ ...
7โ— Found it: table hadd_todo. Let me get its EntitySetName and check the priority/status columns, then total by priority.
8โ— Bash(dataverse api request --path "api/data/v9.2/hadd_todos?$apply=filter(statecode eq 0)/groupby((hadd_priority),aggregate($count as total))")
9  โŽฟ {"value":[{"hadd_priority":100000002,"total":6},{"hadd_priority":100000000,"total":4},{"hadd_priority":100000001,"total":4}]}
10โ— Open Todo items by priority (table hadd_todo, filtered to Active/open records):
11
12  | Priority    | Count  |
13  |-------------|--------|
14  | High        | 6      |
15  | Medium      | 4      |
16  | Low         | 4      |
17  | Total open  | 14     |
18
19  (One additional record, "Do this thing," has no priority set and is marked Inactive, so it's excluded as not open.)

This one took 27 turns. Claude first had to find which table "Todo items" meant, and on the way it hit the leading-slash problem described at the end of this section three times before it found the form that works. The total itself came from an OData $apply aggregate, calculated by Dataverse rather than by fetching every record.

dv-data: writing records. Create, update, delete and upsert, CSV import, loading several related tables in the right order, and sample data. Sample records use made-up @example.com emails.

PROMPT
1Import contacts.csv into the contacts table, matching each Company to an existing account
TEXT
1โ— Read(contacts.csv)
2  โŽฟ firstname,lastname,emailaddress1,company
3    Dan,Kowalski,dan.kowalski@example.com,Contoso
4    ...
5โ— Bash(dataverse data query --table accounts --select "name,accountid")
6โ— The single account is named "Contoso," which matches all 3 rows in contacts.csv.
7โ— Skill(dataverse:dv-data)
8โ— Before I write anything, I need to confirm the target environment โ€” this is the first write action in this session.
9
10  I'm about to create 3 contact records in https://org62b9d1ca.crm.dynamics.com/ (org dev-no-721df8ab),
11  each linked to the existing account Contoso:
12
13  | First | Last     | Email                     | Company โ†’ Account |
14  |-------|----------|---------------------------|-------------------|
15  | Dan   | Kowalski | dan.kowalski@example.com  | Contoso           |
16  | Ella  | Fischer  | ella.fischer@example.com  | Contoso           |
17  | Finn  | Walsh    | finn.walsh@example.com    | Contoso           |
18
19  Is this the correct environment, and should I go ahead?
20
21> Yes, that is the right environment. Go ahead.
22
23โ— Bash(dataverse data create --table contacts --data '{"firstname":"Dan", ...,
24      "parentcustomerid_account@odata.bind":"/accounts(f31f8a9c-...)"}')
25โ— First record created and correctly linked to Contoso. Creating the remaining two.
26โ— All 3 contacts from contacts.csv were imported into the contacts table, each linked to the existing Contoso account.

Claude stopped before its first write in the session and asked me to confirm the environment, as the overview's rules require.

PROMPT
1Add 20 sample Todo items
TEXT
1โ— Bash(dataverse api request --path "api/data/v9.2/EntityDefinitions(LogicalName='hadd_todo')/Attributes?...")
2โ— Bash(dataverse data create --table hadd_todos --data '{"hadd_title":"Water the office plants", ...}')
3  โŽฟ {"id":"e5f33d9e-...","table":"hadd_todos"}
4โ— That worked. Continuing with the remaining 15.
5  ...
6โ— All 20 created. Let me verify the final count.
7โ— Bash(dataverse data count --table hadd_todos)
8  โŽฟ Count: 35
9โ— Added 20 sample Todo items to hadd_todo, mixing Home/Work/Errands categories, Low/Medium/High priorities,
10  due dates through mid-November, and a few marked completed. Table total is now 35 (was 15 before this batch).

This time Claude did not ask again: I had already confirmed the environment earlier in the session.

dv-metadata: the data model. Tables, columns, relationships, forms and views, and reading the schema that is already there. It makes every change in the live environment and then pulls it into the repo, which I come back to below.

PROMPT
1What columns and relationships does the Todo table have?
TEXT
1โ— Skill(dataverse:dv-metadata)
2โ— Bash(dataverse api request --path ".../OneToManyRelationships" ... ".../ManyToOneRelationships" ... ".../ManyToManyRelationships")
3โ— Columns on hadd_todo
4
5  | Display     | Logical Name       | Type     | Notes                                           |
6  |-------------|--------------------|----------|-------------------------------------------------|
7  | Title       | hadd_title         | String   | Primary name column                             |
8  | Description | hadd_description   | String   |                                                 |
9  | Category    | hadd_category      | Picklist | Home=100000000, Work=100000001, Errands=100000002 |
10  | Priority    | hadd_priority      | Picklist | Low=100000000, Medium=100000001, High=100000002   |
11  | Due Date    | hadd_duedate       | DateTime |                                                 |
12  | Completed   | hadd_completed     | Boolean  |                                                 |
13
14  Relationships: none custom โ€” hadd_todo has no lookups to other business tables (no link to Account/Contact)
15  and no N:N relationships. So it's a standalone table โ€” nothing currently ties a Todo item to an Account or Contact.
PROMPT
1Add an Effort choice column to the Todo table, with Small, Medium and Large
TEXT
1โ— Bash(ls .env)
2  โŽฟ No such file or directory
3โ— Bash(dataverse data query --sql "SELECT uniquename, customizationprefix, friendlyname FROM publisher WHERE customizationprefix = 'hadd'")
4โ— Found it โ€” the table lives in solution TodoSolution (and also surfaced via TodoExtensions). Two publishers share
5  the hadd prefix: Haddley and HaddleyDev. Which solution should the new Effort column go into โ€” TodoSolution,
6  TodoExtensions, or something else?
7
8> TodoExtensions
9
10โ— Write(effort_column.json)
11โ— Bash(dataverse api request --method POST --path "api/data/v9.2/EntityDefinitions(LogicalName='hadd_todo')/Attributes"
12      --body-file effort_column.json --header "MSCRM.SolutionUniqueName:TodoExtensions")
13โ— Added the Effort choice column to hadd_todo in the TodoExtensions solution:
14
15  | Display Name | Logical Name | Type     | Options                                                 |
16  |--------------|--------------|----------|---------------------------------------------------------|
17  | Effort       | hadd_effort  | Picklist | Small (100000000), Medium (100000001), Large (100000002) |
18
19  One thing: this working directory isn't a git repo, so I can't do the usual export โ†’ unpack โ†’ commit
20  pull-to-repo step. Want me to export/unpack the TodoExtensions solution into the folder anyway?

Claude would not guess where the column belonged, and asked. Earlier I had added a solution called TodoExtensions, with its own publisher, because the Todo table came from a managed solution whose publisher is read-only. Then, without being asked, Claude offered the second half of the environment-first loop: export the change back into the folder.

dv-solution: moving work between environments. Create a publisher and a solution, export and unpack it into Git, pack and import it into another environment, and check that the import worked.

PROMPT
1Export the TodoExtensions solution and unpack it into this folder
TEXT
1โ— Skill(dataverse:dv-solution)
2โ— Bash(pac auth list)
3โ— Bash(pac solution export --name TodoExtensions --path solutions/TodoExtensions.zip --managed false
4      --environment https://org62b9d1ca.crm.dynamics.com/)
5  โŽฟ Connected to... dev-no-721df8ab
6    Solution export succeeded.
7โ— Export succeeded. Now unpacking into the folder.
8โ— Bash(pac solution unpack --zipfile solutions/TodoExtensions.zip --folder solutions/TodoExtensions --packagetype Unmanaged)
9โ— Bash(rm solutions/TodoExtensions.zip; find solutions/TodoExtensions -type f)
10โ— Bash(grep -c "hadd_effort" solutions/TodoExtensions/Entities/hadd_todo/Entity.xml)
11  โŽฟ 3
12โ— Exported and unpacked TodoExtensions into solutions/TodoExtensions/ โ€” the zip was deleted, leaving the source
13  files (Entity.xml for hadd_todo, Customizations.xml, Solution.xml). Confirmed the new hadd_effort Effort column is in there.

Claude checked pac auth list before exporting, and then checked that the new column was really in the exported XML. Dataverse wrote that XML, not Claude.

dv-security: access. Security roles, application users, business units, and making yourself an admin.

PROMPT
1Which security roles do I have in this environment?
TEXT
1โ— Skill(dataverse:dv-security)
2โ— Bash(dataverse api request --path "api/data/v9.2/WhoAmI")
3โ— Bash(dataverse api request --path "api/data/v9.2/systemusers(20cd6e0f-...)/systemuserroles_association?$select=name,roleid")
4โ— You (admin@p8lf.onmicrosoft.com, in org62b9d1ca.crm.dynamics.com) have these security roles:
5  - System Administrator
6  - System Customizer
7  - Basic User
PROMPT
1Give me the Environment Maker role in this environment
TEXT
1โ— I'll assign Environment Maker to admin@p8lf.onmicrosoft.com on https://org62b9d1ca.crm.dynamics.com/ and verify it landed.
2โ— Bash(pac admin assign-user --user admin@p8lf.onmicrosoft.com --role "Environment Maker"
3      --environment https://org62b9d1ca.crm.dynamics.com/)
4  โŽฟ Connected to... dev-no-721df8ab
5โ— Output reported success, but exit 0 isn't proof โ€” let me verify directly against the user's role list.
6โ— Bash(dataverse api request --path "api/data/v9.2/systemusers(20cd6e0f-...)/systemuserroles_association?$select=name")
7โ— Confirmed โ€” Environment Maker is now on your role list for admin@p8lf.onmicrosoft.com in dev-no-721df8ab,
8  alongside System Administrator, System Customizer, and Basic User.

"Exit 0 isn't proof" comes straight from the skill, which has a section with that title.

dv-admin: running the environment. Bulk delete, retention and archiving, audit, the recycle bin, and organisation settings, limited to an allowlist of 37 settings. It refuses anything outside that list. I asked for one setting on the list and one that is not.

PROMPT
1Is auditing turned on in this environment? Set the audit log retention to 90 days, and turn off autosave
TEXT
1โ— Skill(dataverse:dv-admin)
2โ— Bash(pac org list-settings --filter isauditenabled --environment https://org62b9d1ca.crm.dynamics.com/)
3  โŽฟ isauditenabled Yes
4โ— Auditing is on (isauditenabled = Yes). Now setting the audit log retention to 90 days.
5โ— Bash(pac org update-settings --name auditretentionperiodv2 --value 90 --environment https://org62b9d1ca.crm.dynamics.com/)
6  โŽฟ Setting 'auditretentionperiodv2' updated successfully to '90'.
7โ— Here's where things stand:
8  - Auditing: already turned on (isauditenabled = Yes).
9  - Audit log retention: set to 90 days, confirmed.
10  - Autosave: I can't do this one โ€” isautosaveenabled is explicitly outside dv-admin's allowlisted settings
11    (only 37 specific PPAC toggles are in scope; autosave isn't one of them). You'd need to turn that off
12    yourself via the Power Platform admin center.
PROMPT
1Delete all the completed Todo items
TEXT
1โ— Bash(dataverse data query --table hadd_todos --select "hadd_title,hadd_completed" --filter "hadd_completed eq true")
2โ— Found 3 completed Todo items:
3
4  | Title                | ID                                     |
5  |----------------------|----------------------------------------|
6  | Review pull requests | eaf33d9e-eec9-f111-aaaf-6045bd094f74   |
7  | Return library books | 55e088a7-eec9-f111-aaaf-6045bd094f74   |
8  | Plan weekend trip    | e23960bb-eec9-f111-aaaf-6045bd094f74   |
9
10  I'll delete these 3 records from hadd_todo. Confirm?
11
12> Yes, delete them
13
14โ— Bash(dataverse data delete --table hadd_todos --id eaf33d9e-... --no-confirm  (and the other two))
15  โŽฟ Record deleted successfully.
16โ— All 3 completed Todo items ("Review pull requests," "Return library books," "Plan weekend trip") were deleted.

For three records, Claude did not schedule a bulk delete job. It listed the records, asked, and deleted them one by one.

erp-xpp: Finance and Operations code. Write, compile, deploy and database-sync X++ customisations with pac package commands.

PROMPT
1Create an X++ custom service that returns a customer's credit limit

I did not run this one. The skill only runs on Windows, so it cannot run on my Mac.

Under the hood: A leading slash that breaks on a Mac

The skills document the raw API escape hatch as dataverse api request --path "/api/data/v9.2/WhoAmI". On my Mac that fails with "Absolute URL host '' does not match the environment host", and Claude ran into it three times while totalling the Todo items. Without the leading slash, --path "api/data/v9.2/WhoAmI", it works, and once Claude had found that form it used it for the rest of the session. My guess is that the CLI, which is written in .NET, reads a path that starts with / as an absolute file address on macOS and Linux, so it never treats it as relative to the environment.

My skill and Microsoft's plugin

I read the plugin's skill files after the session, and the dv-metadata skill states its central rule plainly: "Do not write solution XML by hand to create new tables, columns, forms, or views." Its workflow is environment-first:

  1. Make the change in the environment, through the Dataverse metadata API.
  2. Pull the change into the repo with pac solution export and pac solution unpack.
  3. Commit the result.

The reason it gives is that "the environment validates metadata far more reliably than an agent editing XML", and that an exported solution is "generated by Dataverse itself and is always valid". My dv-generate skill from Part 13 does exactly what that rule forbids: it writes the solution XML by hand, offline, and I import the zip. Its long list of hard-won rules about XML that fails to import is the cost of that choice.

The plugin also has a gap that dv-generate fills. Its overview says that tables, forms and views are covered, but that composing a model-driven app itself, with its site map and navigation, "is not yet a first-class skill". dv-generate writes the AppModule and SiteMap along with the table.

dv-generate (Part 13)Microsoft's Dataverse plugin
Who made itMeMicrosoft
Needs an environmentNoYes
How a new table is madeHand-written solution XML, packed into a zip that I importCreated in the environment, then exported and unpacked into the repo
Model-driven app shell (AppModule and SiteMap)YesNot yet
Signing inNoneDevice code for the Dataverse CLI and the PAC CLI
Good forStarting a solution offlineQuestions, records, solutions, security and administration on a real environment

Part of the Claude Code series