dv-admin · diff
git:20260424.f263f94 to git:20260427.1cfb415
68 added, 612 removed. Audit A to A.
---
name: dv-admin
- description: >
- Environment-level Dataverse administration: bulk delete, data retention/archival,
- organization settings, OrgDB settings, recycle bin, and settings-definition overrides.
- Covers 37 allowlisted PPAC toggles — audit, plugin trace, typeahead/quick find, canvas/flow solutions,
- audit retention, MCP, search, Fabric, Work IQ, M365 Copilot, TDS endpoint, attachment security,
- ownership across BUs, address records, delete users, Excel AI import, block unmanaged, app/plan security roles.
- Use when: "bulk delete", "clean up old records", "schedule deletion", "pause/cancel/resume bulk delete", "pac data",
- "retention", "archive", "data lifecycle", "audit retention", "audit log retention",
- "org settings", "organization settings", "environment settings", "list-settings", "update-settings",
- "enable audit", "audit status", "read auditing", "user access audit", "plugin trace",
- "typeahead", "lookup delay", "quick find", "single table search", "leading wildcard",
- "canvas apps in solutions", "cloud flows in solutions", "email validation",
- "recycle bin", "cleanup interval", "recyclebinconfig",
- "orgdb settings", "search settings", "mcp settings", "mcp advanced",
- "fabric settings", "link to fabric", "fabric virtual tables",
- "work iq", "dataverse intelligence", "m365 copilot data",
- "tds endpoint", "attachment security", "record ownership", "ownership across business units",
- "address records", "block unmanaged customizations", "delete disabled users", "excel import ai",
- "app level security roles", "plan level security roles".
- Do not use when: deleting individual records (use dv-data), deleting tables (use dv-metadata),
- exporting/importing data files (use dv-solution), querying records (use dv-query),
- assigning roles or self-elevating (use dv-security), creating sample data (use dv-data),
- tenant governance like DLP or environment lifecycle (use pac admin --help).
+ description: Environment-level Dataverse administration — bulk delete, retention/archival, organization settings, OrgDB settings, recycle bin, audit, and the 37 allowlisted PPAC toggles. Use when the user wants to clean up data at scale, configure audit, change environment settings, or manage retention policies.
---
# Skill: Environment Admin — Bulk Delete, Retention, Org Settings, OrgDB, Recycle Bin
+ > ## ⚠️ Critical safety rules — read first
+ >
+ > 1. **Bulk delete is irreversible and bypasses the recycle bin.** `pac data bulk-delete schedule` without `--fetchxml` deletes every record in the table. Refuse to run until the user explicitly says ALL (or ALL RECORDS) **and** the entity logical name — e.g., `"yes, delete ALL records in contact"`. Bare `"yes"` rejected. Empty-filter FetchXML does NOT bypass this gate. See [Bulk Delete Commands](#bulk-delete-commands) for the full rule and disambiguation flow.
+ > 2. **Settings allowlist is hard.** Only the 37 PPAC toggles in [Allowed settings](#allowed-settings--hard-allowlist) may be read or updated. Any other setting **must be refused**: *"That setting is out of scope for dv-admin. Use the Power Platform admin center."*
+ > 3. **Recycle bin disable is PATCH, never DELETE.** `PATCH statecode=1, statuscode=2, isreadyforrecyclebin=false`. DELETE enqueues async opt-out and orphans per-entity configs — see [`references/recycle-bin.md`](references/recycle-bin.md).
+ > 4. **System tables warning.** Unfiltered bulk delete on `systemuser`, `businessunit`, `organization`, or `role` breaks the environment. Warn additionally before running.
+
**Four mechanisms — pick based on where the setting lives:**
| Mechanism | Use for | How |
|---|---|---|
| **PAC CLI** (`pac org update-settings` / `list-settings`) | Columns on the `organization` entity (audit, plugin trace, typeahead, quick find, canvas/flow solutions, email validation, audit retention) | `--name <column> --value <value>` — accepts any org column, not just the legacy audit ones |
| **Python SDK — OrgDB XML** | Keys inside the `orgdborgsettings` XML blob (MCP, search, Fabric, Work IQ, TDS endpoint, attachment security, ownership, address records, block unmanaged, delete users, Excel AI) | Read XML → parse → modify → PATCH whole blob back on `organizations({id})` |
| **Python SDK — recyclebinconfigs** | Recycle bin on/off + retention days | CREATE/PATCH `recyclebinconfigs` entity record |
| **Python SDK — settingdefinition + organizationsettings** | App-level / plan-level security role toggles | Look up `settingdefinition` by `uniquename` → CREATE or PATCH `organizationsettings` row with `value` |
Do NOT write Python scripts for operations PAC CLI can handle. Do NOT mix mechanisms (e.g., don't hand-PATCH an org column that PAC CLI already covers).
## Skill boundaries
- | Need | Use instead |
- |---|---|
- | Delete or update individual records | **dv-data** |
- | Create or delete tables, columns, relationships | **dv-metadata** |
- | Query or read records | **dv-query** |
- | Export or import solutions / data files | **dv-solution** |
- | Assign security roles or self-elevate | **dv-security** |
- | Create sample or seed data | **dv-data** |
- | Tenant governance (DLP, env lifecycle) | `pac admin --help` |
+ Use **dv-data** for record CRUD and sample data, **dv-metadata** for tables / columns / relationships, **dv-query** for reading records, **dv-solution** for solution export/import, **dv-security** for roles and self-elevate, `pac admin --help` for tenant governance (DLP, env lifecycle).
## Allowed settings — hard allowlist
- This skill is only permitted to **read or update** the 37 PPAC toggles listed below (mapped to 35 unique backend keys — `SearchAndCopilotIndexMode` covers two toggles, `auditretentionperiodv2` covers two). Any request to change any other setting (session timeout, autosave, upload size, abort timeout, archival, shadow lake, activities features, synapse, or any org column / OrgDB key / `organizationsettings` override not in this table) **must be refused** with: *"That setting is out of scope for dv-admin. Use the Power Platform admin center."*
-
- Reading other settings is also out of scope — do not generate `pac org list-settings` without `--filter`, and do not dump the whole `orgdborgsettings` XML when the user's question is about a specific non-allowlisted setting.
+ The 37 PPAC toggles below are the only ones this skill may read or update (35 unique backend keys — `SearchAndCopilotIndexMode` and `auditretentionperiodv2` each cover two toggles). Examples that are **out of scope and must be refused** per the safety rule above: `sessiontimeoutinmins`, `isautosaveenabled`, `maxuploadfilesize`, `inaborttimeoutinmins`, `IsArchivalEnabled`, `IsShadowLakeEnabled`, `EnableActivitiesFeatures`. Do not run `pac org list-settings` without `--filter`, and do not dump the whole `orgdborgsettings` XML for a non-allowlisted setting.
### Organization entity columns — use PAC CLI (14)
Use `pac org list-settings --filter <column>` to read, `pac org update-settings --name <column> --value <value>` to write. PAC CLI accepts any column on the organization entity — not just the legacy audit ones.
| # | PPAC label | Column | Type |
|---|---|---|---|
| 1 | Start Auditing | `isauditenabled` | bool |
| 2 | Audit user access (Log access) | `isuseraccessauditenabled` | bool |
| 3 | Start Read Auditing (Read logs to Purview) | `isreadauditenabled` | bool |
| 4 | Plugin trace log setting | `plugintracelogsetting` | int: `0` Off, `1` Exception, `2` All |
| 5 | Single table search option | `tablescopeddvsearchinapps` | bool |
| 6 | Prevent slow keyword filter for quick find terms | `allowleadingwildcardsinquickfind` | int: `0` prevent, `1` allow (UI "prevent=On" flips to `0`) |
| 7 | Quick Find record limits | `quickfindrecordlimitenabled` | bool |
| 8 | Use quick find view for searching on grids/subgrids | `usequickfindviewforgridsearch` | bool |
| 9 | Canvas apps in Dataverse solutions by default | `enablecanvasappsinsolutionsbydefault` | bool |
| 10 | Cloud flows in Dataverse solutions by default | `enableflowsinsolutionbydefault` | bool (note: `solution` singular) |
| 11 | Enable email address validation (preview) | `isemailaddressvalidationenabled` | bool |
| 12 | Minimum number of characters to trigger typeahead | `lookupcharactercountbeforeresolve` | int (0–MAX_INT, null = feature off) |
| 13 | Delay between character inputs that trigger a search | `lookupresolvedelayms` | int ms (default 250) |
| 14 | Audit log retention policy / Custom retention period (days) | `auditretentionperiodv2` | int days (`-1` = Forever; presets 30/90/180/365/730/2555; max 365000) |
### OrgDB XML keys — use Python SDK on `orgdborgsettings` blob (17)
- Read/modify/PATCH the XML blob on `organizations({id})`. PascalCase is significant — `IsMCPEnabled`, not `IsMcpEnabled`.
-
- | # | PPAC label | XML key | Type |
- |---|---|---|---|
- | 15 | Allow MCP clients to interact with Dataverse MCP server | `IsMCPEnabled` | bool |
- | 16 | Advanced Settings (non-Copilot Studio MCP clients) | `IsMCPPreviewEnabled` | bool |
- | 17 | Dataverse search / Search for records in Microsoft 365 apps | `SearchAndCopilotIndexMode` | int 0–3 (see truth table below — one key, two UI toggles) |
- | 18 | Link Dataverse tables with Microsoft Fabric workspace | `IsLinkToFabricEnabled` | bool |
- | 19 | Define Dataverse virtual tables using Fabric OneLake data | `IsFabricVirtualTableEnabled` | bool |
- | 20 | Allow data availability in Microsoft 365 Copilot | `ShowDataInM365Copilot` | bool |
- | 21 | Turn on Dataverse intelligence (Work IQ) for agents | `EnableWorkIQ` | bool |
- | 22 | Block unmanaged customizations in environment | `IsLockdownOfUnmanagedCustomizationEnabled` | bool |
- | 23 | Enable security on Attachment entity | `EnableSecurityOnAttachment` | bool |
- | 24 | Enable TDS endpoint | `EnableTDSEndpoint` | bool |
- | 25 | Enable user level access control for TDS endpoint | `AllowAccessToTDSEndpoint` | bool |
- | 26 | Record ownership across business units | `EnableOwnershipAcrossBusinessUnits` | bool |
- | 27 | Disable empty address record creation | `CreateOnlyNonEmptyAddressRecordsForEligibleEntities` | bool |
- | 28 | Enable deletion of address records | `EnableDeleteAddressRecords` | bool |
- | 29 | Block deletion of OOB attribute maps | `BlockDeleteManagedAttributeMap` | bool |
- | 30 | Enable delete disabled users | `EnableSystemUserDelete` | bool |
- | 31 | Import Excel to existing table with AI-assisted mapping | `IsExcelToExistingTableWithAssistedMappingEnabled` | bool |
+ PascalCase is significant (`IsMCPEnabled`, not `IsMcpEnabled`). Full key list with PPAC labels and types lives in [`references/orgdb-settings.md`](references/orgdb-settings.md). Notable keys: `IsMCPEnabled`, `IsMCPPreviewEnabled`, `SearchAndCopilotIndexMode`, `IsLinkToFabricEnabled`, `IsFabricVirtualTableEnabled`, `ShowDataInM365Copilot`, `EnableWorkIQ`, `IsLockdownOfUnmanagedCustomizationEnabled`, `EnableSecurityOnAttachment`, `EnableTDSEndpoint`, `AllowAccessToTDSEndpoint`, `EnableOwnershipAcrossBusinessUnits`, `CreateOnlyNonEmptyAddressRecordsForEligibleEntities`, `EnableDeleteAddressRecords`, `BlockDeleteManagedAttributeMap`, `EnableSystemUserDelete`, `IsExcelToExistingTableWithAssistedMappingEnabled`.
- **`SearchAndCopilotIndexMode` truth table** (one int encodes two UI toggles):
+ **`SearchAndCopilotIndexMode`** is one int (0–3) that encodes two UI toggles — Dataverse search × M365 Copilot search:
- | Value | "Dataverse search" | "Search for records in M365 apps" (Copilot) |
+ | Value | Dataverse search | M365 Copilot search |
|---|---|---|
| `0` | Off | On |
| `1` | On | On |
| `2` | Off | Off |
| `3` | On | Off |
- ### recyclebinconfigs entity — use Python SDK (2, **org-level only**)
-
- These two toggles operate on the **organization-level** `recyclebinconfigs` row (filtered by the organization entity's MetadataId). Per-table recycle bin enable/disable is **out of scope** — PPAC only exposes the org-level on/off + cleanup days. Refuse requests like "enable recycle bin for `contact` only".
+ ### recyclebinconfigs entity — use Python SDK (2, org-level only)
- | # | PPAC label | Field | Notes |
- |---|---|---|---|
- | 32 | Keep deleted Dataverse records (on/off) | `statecode` + `statuscode` + `isreadyforrecyclebin` | Org-level row only. See "Recycle Bin Configuration" section — always send `isreadyforrecyclebin: true` on enable |
- | 33 | Keep deleted records (days) | `cleanupintervalindays` | Org-level row only. int days; `-1` = never auto-purge; max 30 (when UI shows "30 days" with retention, stored as `-1`) |
+ Two toggles operate on the org-level `recyclebinconfigs` row (filtered by the organization entity's MetadataId): on/off (`statecode` + `statuscode` + `isreadyforrecyclebin`) and cleanup days (`cleanupintervalindays`). Per-table toggles are **out of scope** — refuse requests like "enable recycle bin for `contact` only". Full lifecycle in [`references/recycle-bin.md`](references/recycle-bin.md).
### settingdefinition + organizationsettings — use Python SDK (2)
- These live in a join: `settingdefinition` holds the definition (default value, datatype), `organizationsettings` holds the org-level override. No override row means the defaultvalue applies.
-
- | # | PPAC label | `settingdefinition.uniquename` | Type |
- |---|---|---|---|
- | 34 | Enable app level security roles for canvas apps | `PowerAppsAppLevelSecurityRolesEnabled` | bool (string `"true"`/`"false"`) |
- | 35 | Enable plan level security roles for plan designer | `PlanShareSecurityRolesEnabled` | bool (string `"true"`/`"false"`) |
-
- Bulk delete, retention/archival jobs, and role assignment are covered elsewhere in this skill and remain in scope — this allowlist is about **environment/org/orgdb settings** only.
-
- ## Preview Before Running — Scope by Operation Type
+ Two allowlisted toggles, both bool stored as string: `PowerAppsAppLevelSecurityRolesEnabled` (canvas apps), `PlanShareSecurityRolesEnabled` (plan designer). Read default from `settingdefinition`, CREATE/PATCH `organizationsettings` for the override. Full Python in [`references/settings-overrides.md`](references/settings-overrides.md).
- Two rules, different strictness:
+ ## Preview Before Running
- - **Destructive / stateful operations** (bulk delete schedule/cancel/pause/resume, org-settings update, OrgDB-settings update, recycle bin toggle or interval change, role assignment, self-elevate, retention set/enable-entity) — preview the action in plain prose: what's changing, new value, and which environment(s), using placeholders (`<ENV_URL>`, `<USER_EMAIL>`) for anything unknown. Ask for confirmation and missing values in the same turn. Skip the raw command block; the user shouldn't need to read CLI syntax to approve a change.
- - **Read-only operations** (list-settings, list jobs, show job, read recycle bin status, read OrgDB settings) — a one-sentence prose preview is enough.
+ - **Destructive / stateful** (bulk delete schedule/cancel/pause/resume, settings updates, recycle bin toggle, role assignment, self-elevate, retention set) — preview in prose: what's changing, new value, target environment(s). Use placeholders (`<ENV_URL>`) for unknowns and ask for missing values in the same turn. Skip the raw CLI block.
+ - **Read-only** (list-settings, show job, read OrgDB / recycle bin status) — one-sentence prose preview is enough.
- **Key principle:** the user should be able to evaluate what's about to happen from your first response. A bare *"which environment?"* fails that test; a one-line prose preview passes it.
+ The user must be able to evaluate the action from your first response. A bare *"which environment?"* fails; a one-line prose preview passes.
### Examples
- <example operation="pause bulk delete job (destructive — all info given)">
- <user>Pause the bulk delete job with ID 12345678-1234-1234-1234-123456789012</user>
- <bad>The command requires approval. Please confirm to pause the job.</bad>
- <good>I'll pause bulk delete job `12345678-1234-1234-1234-123456789012` on the active environment. Confirm to proceed.</good>
- </example>
-
- <example operation="read recycle bin status (read-only — prose is enough)">
- <user>What's the current recycle bin status?</user>
- <bad>Please provide your environment URL so I can check.</bad>
- <bad>Runs `pac org fetch` against `recyclebinconfig` — PAC CLI does NOT manage recycle bin. Always use the Web API via Python.</bad>
- <good>Recycle bin lives in the `recyclebinconfigs` entity — PAC CLI can't read it. I'll run the "Read Recycle Bin Status" snippet in the Recycle Bin Configuration section below (read-only, no confirmation needed). Provide your environment URL.</good>
- </example>
-
- <example operation="audit status across N environments (read-only but multi-call — teach the pattern)">
- <user>Tell me audit status of these 5 environments</user>
- <bad>Runs `pac env select` then `pac org fetch --entity organization --attributes isauditenabled` once per environment, sequentially, across N separate bash calls.</bad>
- <bad>Starts with Python / `urllib` / the SDK because "audit status" sounds like a multi-attribute query. Then falls back to `pac org fetch` when that fails, then PowerShell. All forbidden.</bad>
- <bad>Uses a `for env_id in ...` bash loop (sequential) instead of `&`-backgrounded parallel calls.</bad>
- <good>I'll run `pac org list-settings --filter audit` in parallel across all 5 environments (one `&`-batch, single `wait`), then render each env's audit block as a merged table. `--filter audit` returns every audit-related setting in one call (`isauditenabled`, `auditretentionperiodv2`, `isuseraccessauditenabled`, etc.) so we don't need multiple passes. `pac org list-settings` is the only command for reading org settings — never `pac org fetch`, `pac env select`, Python, PowerShell, or Web API.</good>
- </example>
-
- <example operation="disable MCP (destructive OrgDB write — prose + snippet reference)">
- <user>Disable MCP for my environment</user>
- <bad>Please provide your environment URL so I can disable MCP.</bad>
- <good>MCP is the `IsMCPEnabled` key in the `orgdborgsettings` XML blob — PAC CLI can't modify it. I'll run the "Update or add an OrgDB setting" snippet in the OrgDB Settings section with `SETTING_NAME="IsMCPEnabled"`, `SETTING_VALUE="false"`. Provide your environment URL and confirm to proceed.</good>
- </example>
+ **Pause bulk delete (destructive, ID supplied):**
+ - ❌ "The command requires approval. Please confirm to pause the job."
+ - ✅ "I'll pause bulk delete job `<job-id>` on the active environment. Confirm to proceed."
- ## CRITICAL: How to Read or Update Org Settings (Single or Multi-Environment)
+ **Audit status across N environments (read-only, multi-call):**
+ - ❌ Sequential `pac org fetch` per env, or starting with Python/SDK because it "feels like a query."
+ - ✅ "I'll run `pac org list-settings --filter audit` in parallel across all N environments (one `&`-batch, single `wait`)."
- **Org settings = always `pac org list-settings` / `pac org update-settings`. No exceptions.** Single setting or a whole category, single env or many — this pair of commands covers all of it. If you are about to write Python, FetchXML, PowerShell, or any HTTP call to read or write an org column, STOP: you are off-rails.
+ ## How to Read or Update Org Settings
- **Single setting on one environment:**
+ **Org settings always go through `pac org list-settings` / `pac org update-settings`** — never raw Web API, FetchXML, PowerShell, or Python for org columns. Use `--filter <substring>` for category reads in one call. Multi-environment work runs in parallel via `&` + `wait` in ONE bash call.
+ **Single setting:**
```bash
- pac org list-settings --filter isauditenabled --environment https://org1.crm.dynamics.com
+ pac org list-settings --filter isauditenabled --environment <url>
```
- **Category / multi-attribute read (e.g., "audit status", "all MCP settings") — substring filter in one call:**
-
+ **Category read** (returns every match, e.g. all audit settings in one call):
```bash
- pac org list-settings --filter audit --environment https://org1.crm.dynamics.com
+ pac org list-settings --filter audit --environment <url>
```
- `--filter` is a substring match on the setting name. `--filter audit` returns everything whose name contains "audit" (`isauditenabled`, `auditretentionperiodv2`, `isuseraccessauditenabled`, `isreadauditenabled`, etc.). Prefer this over running N separate `--filter <one>` calls when the user asks about a category.
-
- **Multiple environments — ALWAYS parallel in ONE bash call:**
-
+ **Multi-environment — parallel in ONE bash call:**
```bash
- pac org list-settings --filter audit --environment https://org1.crm.dynamics.com &
- pac org list-settings --filter audit --environment https://org2.crm.dynamics.com &
- pac org list-settings --filter audit --environment https://org3.crm.dynamics.com &
+ pac org list-settings --filter audit --environment <url1> &
+ pac org list-settings --filter audit --environment <url2> &
+ pac org list-settings --filter audit --environment <url3> &
wait
```
- Then parse each environment's output and render a table. Same pattern for N environments: N backgrounded calls, one `wait`, one merged table.
-
- **NEVER do any of these for org settings — no exceptions, even when `list-settings` feels limiting:**
- - `pac env select` then `pac org fetch` — WRONG, use `pac org list-settings --environment <url>`
- - `pac org fetch` with FetchXML to query the `organization` entity — WRONG, PAC CLI reads org settings via `list-settings` only
- - `pac org who` — shows connection info, NOT settings
- - `curl` / `urllib` / `requests` to the Web API — WRONG for org columns, use `pac org list-settings`
- - `pac auth token` + raw HTTP — WRONG, PAC CLI handles auth internally
- - PowerShell `Invoke-RestMethod` — WRONG, use PAC CLI directly
- - Python `DataverseClient` / `get_token()` / any Azure Identity call for org columns — WRONG, use PAC CLI
- - Separate bash calls per environment / a `for` loop with sequential calls — WRONG, use `&` and `wait` in ONE call
-
- **If `pac org list-settings` fails for a setting**, that setting is NOT an org column — it lives elsewhere (OrgDB XML blob, `recyclebinconfigs`, `settingdefinition`). See the mechanism routing table below. Do not fall back to Web API / PowerShell / FetchXML.
+ If `pac org list-settings` fails for a setting, that setting is NOT an org column — check the mechanism routing in the four-mechanism table at the top, then use the appropriate Python pattern. Do NOT fall back to Web API, PowerShell, FetchXML, or `pac org fetch` for org columns.
---
## Prerequisites
- - PAC CLI **latest version (.NET Framework build)** — `pac data bulk-delete` and `pac data retention` commands are only in the .NET Framework build (not the `dotnet tool` cross-platform version). Check your version:
- ```bash
- pac help # look for "Version: x.x.x (.NET Framework ...)"
- ```
- If it shows `.NET 10` or `.NET 8` instead of `.NET Framework`, the `pac data` commands will not be available. To update to the latest .NET Framework build:
- ```bash
- pac install latest # downloads latest NuGet package
- pac use latest # switches to the latest installed version
- ```
- - Authenticated (`pac auth create`)
- - A Dataverse environment with System Administrator privilege
- - Active auth profile: `pac auth list`
-
- ---
+ - PAC CLI **latest .NET Framework build** — `pac data bulk-delete` and `pac data retention` are only in the .NET Framework build, not the `dotnet tool` cross-platform version. Check with `pac help` (look for "Version: x.x.x (.NET Framework ...)"); if it shows `.NET 10` or `.NET 8`, run `pac install latest && pac use latest` to switch.
+ - Authenticated (`pac auth create`), active profile (`pac auth list`), and System Administrator privilege on the target environment.
## Multi-Environment Operations — Always Parallel
- The pattern above applies to ALL multi-environment operations, not just org settings. For updates:
-
- ```bash
- # UPDATE settings on multiple environments — ONE bash call, all parallel
- pac org update-settings --name isauditenabled --value true --environment https://org1.crm.dynamics.com &
- pac org update-settings --name isauditenabled --value true --environment https://org2.crm.dynamics.com &
- pac org update-settings --name isauditenabled --value true --environment https://org3.crm.dynamics.com &
- wait
- ```
+ The same `&` + `wait` pattern from `list-settings` applies to every multi-env operation (`update-settings`, `bulk-delete`, etc.) — N backgrounded calls in ONE bash call, never sequential or `for` loops.
---
## Common Mistakes — Do NOT Use These
These flags do not exist. Using them will produce errors.
### Bulk Delete
| Wrong | Correct |
|-------|---------|
| `--filter` | use `--fetchxml` with a FetchXML string |
| `--query` / `--where` / `--condition` | use `--fetchxml` |
| `--date` / `--before` / `--older-than` | encode date in FetchXML `<condition>` |
| `--job-id` | use `--id` |
| `--all` / `--purge` / `--truncate` | omit `--fetchxml` to target all records (warn user first) |
### Retention
| Wrong | Correct |
|-------|---------|
| `--fetchxml` | use `--criteria` (same FetchXML format, different flag name) |
| `--filter` / `--query` / `--policy` | use `--criteria` |
| `--enable` / `--activate` | use `pac data retention enable-entity` |
| `--table` | use `--entity` |
| `--operation-id` / `--job-id` / `--guid` | use `--id` |
### Org Settings
| Wrong | Correct |
|-------|---------|
| `--enable-audit` / `--audit` | use `--name isauditenabled --value true` |
| `--trace` / `--plugin-trace` / `--logging` | use `--name plugintracelogsetting --value 2` |
| `--setting` / `--key` / `--flag` | use `--name` |
| String values like `"all"` or `"enabled"` for option sets | use integers: `0`, `1`, `2` |
---
## Bulk Delete Commands
### Schedule a Bulk Delete Job
```bash
pac data bulk-delete schedule --entity activitypointer \
--fetchxml "<fetch><entity name='activitypointer'><filter><condition attribute='createdon' operator='lt' value='2024-01-01'/></filter></entity></fetch>"
pac data bulk-delete schedule --entity email \
--fetchxml "<fetch><entity name='email'><filter><condition attribute='createdon' operator='lt' value='2024-06-01'/></filter></entity></fetch>" \
--job-name "Cleanup old emails" --recurrence "FREQ=DAILY;INTERVAL=1"
```
| Argument | Alias | Required | Description |
|----------|-------|----------|-------------|
| `--entity` | `-e` | Yes | Logical name of the table |
| `--fetchxml` | `-fx` | No | FetchXML filter. **See the hard-stop rule below — if omitted, ALL records in the table are deleted.** |
| `--job-name` | `-jn` | No | Descriptive name for the job |
| `--start-time` | `-st` | No | ISO 8601 start time. Defaults to now |
| `--recurrence` | `-r` | No | RFC 5545 pattern (e.g., `FREQ=DAILY;INTERVAL=1`) |
| `--environment` | `-env` | No | Target environment URL |
### Hard stop: no `--fetchxml` means ALL records
- A `pac data bulk-delete schedule` without `--fetchxml` targets every record in the table and is irreversible. Bulk delete does not go through the recycle bin. Apply this gate before running:
+ `pac data bulk-delete schedule` without `--fetchxml` targets every record in the table and is irreversible (does not go through recycle bin). Required gate:
- 1. **Refuse to run until the user explicitly acknowledges.** The acknowledgement must include both the word ALL (or ALL RECORDS) **and** the entity logical name. Example accepted: `"yes, delete ALL records in contact"`. Example rejected: a bare `"yes"` or `"proceed"`.
- 2. **Disambiguate first.** If the user's ask is vague ("clean up old emails", "remove stale accounts"), ask clarifying questions (date cutoff? statecode filter? owner?) and draft a FetchXML before showing any `bulk-delete schedule` command.
- 3. **Do not synthesize empty-filter FetchXML to bypass the gate.** An empty `<filter/>` or `<filter><condition ...><value/></condition></filter>` still targets every record — that counts as "no `--fetchxml`" for this rule.
- 4. **Scope.** This gate applies to `pac data bulk-delete schedule` only. `cancel`, `pause`, `resume`, `show`, and `list` don't need it.
+ 1. **Refuse until the user explicitly acknowledges** with the word ALL (or ALL RECORDS) **and** the entity logical name — e.g., `"yes, delete ALL records in contact"`. Bare `"yes"` rejected.
+ 2. **Disambiguate vague asks** ("clean up old emails") — propose a FetchXML filter with date / statecode / owner conditions before showing any command.
+ 3. **Empty-filter FetchXML doesn't bypass the gate** — `<filter/>` or `<filter><condition><value/></condition></filter>` still targets every record.
+ 4. **Scope:** applies to `bulk-delete schedule` only. `cancel`, `pause`, `resume`, `show`, `list` don't need it.
- For system tables (`systemuser`, `businessunit`, `organization`, `role`), warn additionally that an unfiltered bulk delete will break the environment.
+ For system tables (`systemuser`, `businessunit`, `organization`, `role`), additionally warn that unfiltered bulk delete breaks the environment.
### Manage Jobs
```bash
pac data bulk-delete list --environment https://myorg.crm.dynamics.com
pac data bulk-delete show --id <job-id>
pac data bulk-delete pause --id <job-id>
pac data bulk-delete resume --id <job-id>
pac data bulk-delete cancel --id <job-id>
```
---
## Retention / Archival Commands
Data retention moves old records to long-term storage without permanently deleting them.
### Agentic Flow
```
Step 1: pac data retention enable-entity --entity activitypointer
Step 2: pac data retention list
Step 3: pac data retention set --entity activitypointer --criteria "<fetchxml>..."
Step 4: pac data retention show --id <config-id>
```
### Commands
```bash
pac data retention enable-entity --entity activitypointer --environment https://myorg.crm.dynamics.com
pac data retention set --entity activitypointer \
--criteria "<fetch><entity name='activitypointer'><filter><condition attribute='createdon' operator='lt' value='2023-01-01'/></filter></entity></fetch>"
pac data retention list --environment https://myorg.crm.dynamics.com
pac data retention show --id <config-id>
pac data retention status --id <operation-id>
```
| Argument | Alias | Required | Description |
|----------|-------|----------|-------------|
| `--entity` | `-e` | Yes | Logical name of the table |
| `--criteria` | `-c` | Yes | FetchXML defining which records to archive |
| `--start-time` | `-st` | No | ISO 8601 start time. Defaults to now |
| `--recurrence` | `-r` | No | RFC 5545 recurrence pattern |
| `--environment` | `-env` | No | Target environment URL |
### Retention vs Bulk Delete
| Scenario | Use |
|----------|-----|
| Data no longer needed, permanently delete | **Bulk Delete** |
| Data must be preserved for compliance | **Retention** (archive) |
---
## Organization Settings Commands
### List Settings
```bash
pac org list-settings --environment https://myorg.crm.dynamics.com
pac org list-settings --filter isauditenabled --environment https://myorg.crm.dynamics.com
```
### Update a Setting
```bash
pac org update-settings --name isauditenabled --value true --environment https://myorg.crm.dynamics.com
pac org update-settings --name plugintracelogsetting --value 2 --environment https://myorg.crm.dynamics.com
```
- #### Arguments
-
- | Argument | Alias | Required | Description |
- |----------|-------|----------|-------------|
- | `--name` | `-n` | Yes | Setting name (e.g., `isauditenabled`) |
- | `--value` | `-v` | Yes | New value (`true`/`false` for booleans, integer for option sets) |
- | `--environment` | `-env` | No | Target environment URL or ID |
-
- #### Allowed Settings (14 org columns)
-
- `pac org update-settings --name <column>` accepts any column on the `organization` entity, not just the legacy audit ones — verified against real orgs.
-
- | Setting Name | Type | Values | PPAC label |
- |-------------|------|--------|------------|
- | `isauditenabled` | bool | `true` / `false` | Start Auditing |
- | `isuseraccessauditenabled` | bool | `true` / `false` | Audit user access (Log access) |
- | `isreadauditenabled` | bool | `true` / `false` | Start Read Auditing (Read logs to Purview) |
- | `plugintracelogsetting` | option | `0` Off, `1` Exception, `2` All | Plugin trace log setting |
- | `tablescopeddvsearchinapps` | bool | `true` / `false` | Single table search option |
- | `allowleadingwildcardsinquickfind` | int | `0` = prevent slow filter (UI "Prevent" ON), `1` = allow | Prevent slow keyword filter for quick find terms |
- | `quickfindrecordlimitenabled` | bool | `true` / `false` | Quick Find record limits |
- | `usequickfindviewforgridsearch` | bool | `true` / `false` | Use quick find view for searching on grids/subgrids |
- | `enablecanvasappsinsolutionsbydefault` | bool | `true` / `false` | Canvas apps in Dataverse solutions by default |
- | `enableflowsinsolutionbydefault` | bool | `true` / `false` | Cloud flows in Dataverse solutions by default (note: `solution` singular, no `s`) |
- | `isemailaddressvalidationenabled` | bool | `true` / `false` | Enable email address validation (preview) |
- | `lookupcharactercountbeforeresolve` | int | `0`–MAX_INT (null = feature off) | Minimum number of characters to trigger typeahead search |
- | `lookupresolvedelayms` | int | milliseconds (default `250`) | Delay between character inputs that trigger a search |
- | `auditretentionperiodv2` | int | `-1` = Forever; presets `30`, `90`, `180`, `365`, `730`, `2555`; custom integer `30`–`365000` (PPAC validates this range; `< 30` or `> 365000` fails) | Audit log retention policy / Custom retention period (days) |
-
- Any other org column (`isautosaveenabled`, `maxuploadfilesize`, `sessiontimeoutenabled`, `sessiontimeoutinmins`, `inaborttimeoutenabled`, `inaborttimeoutinmins`, etc.) is **out of scope** — refuse and direct the user to the Power Platform admin center.
-
- ### Batch Workflow
+ **Args:** `--name <column>` (required), `--value <value>` (required; `true`/`false` for bool, int for option sets), `--environment <url>` (optional). Allowed columns are the 14 listed in [Allowed settings](#allowed-settings--hard-allowlist) — anything else is out of scope.
- ```
- Step 1: pac admin list -> Get all environments
- Step 2: Filter by type or name -> Identify targets
- Step 3: For updates: confirm with user first
- Step 4: Run ALL commands in parallel (see Multi-Environment section above)
- Step 5: Report summary as a table
- ```
+ **Batch workflow:** `pac admin list` → filter targets → confirm → run all `update-settings` calls in parallel (`&` + `wait`) → render summary table.
---
## Advanced Settings (Python SDK — PAC CLI Cannot Handle These)
- Three patterns require Python SDK:
-
- | Setting type | Where it lives | How to update |
- |---|---|---|
- | Top-level org columns (14 allowlisted — audit, typeahead, quick find, canvas/flow solutions, email validation, audit retention, etc.) | Organization entity columns | **PAC CLI** — `pac org update-settings --name <column>` (accepts any org column) |
- | OrgDB settings (17 allowlisted — MCP, search, Fabric, Work IQ, TDS, attachment security, ownership, address records, block unmanaged, delete users, Excel AI) | XML inside `orgdborgsettings` column | **Python SDK** — fetch XML, parse, modify, PATCH back |
- | Recycle bin (**org-level only**) | `recyclebinconfigs` entity, row filtered by the organization entity's MetadataId | **Python SDK** — CREATE/PATCH `recyclebinconfigs(<id>)`. Per-table toggles are out of scope — PPAC only exposes the org-level on/off + cleanup days |
- | Settings-definition overrides (2 allowlisted — app/plan security roles) | `settingdefinition` + `organizationsettings` join | **Python SDK** — look up `settingdefinitionid` by `uniquename`, then CREATE/PATCH `organizationsettings` row |
-
- ---
+ OrgDB, recycle bin, and settings-definition overrides each need raw Web API or the Python SDK — PAC CLI does not cover them. The four-mechanism routing table at the top of this skill ([§ Skill](#skill-environment-admin--bulk-delete-retention-org-settings-orgdb-recycle-bin)) maps each setting to its mechanism. Sub-sections below summarise the patterns and link to the references for full Python.
### OrgDB Settings (orgdborgsettings XML)
- Settings like search mode, MCP, copilot features, fabric, and retention live inside the `orgdborgsettings` XML blob. The XML uses **direct PascalCase elements** (NOT `<pair>` tags):
-
- ```xml
- <OrgSettings>
- <IsMCPEnabled>true</IsMCPEnabled>
- <SearchAndCopilotIndexMode>0</SearchAndCopilotIndexMode>
- <IsLinkToFabricEnabled>true</IsLinkToFabricEnabled>
- <IsFabricVirtualTableEnabled>false</IsFabricVirtualTableEnabled>
- </OrgSettings>
- ```
-
- **Read all OrgDB settings:**
-
- ```python
- import os, sys, json, urllib.request
- from xml.etree import ElementTree as ET
- sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
- from auth import get_token, load_env # SDK does not support orgdborgsettings XML blob
-
- load_env()
- env_url = os.environ["DATAVERSE_URL"].rstrip("/")
- token = get_token()
-
- req = urllib.request.Request(
- f"{env_url}/api/data/v9.2/organizations?$select=organizationid,orgdborgsettings",
- headers={"Authorization": f"Bearer {token}", "Accept": "application/json"},
- )
- with urllib.request.urlopen(req) as resp:
- org = json.loads(resp.read())["value"][0]
-
- root = ET.fromstring(org["orgdborgsettings"])
- for child in sorted(root, key=lambda c: c.tag):
- print(f" {child.tag} = {child.text}", flush=True)
- ```
-
- **Update or add an OrgDB setting:**
-
- ```python
- import os, sys, json, urllib.request, urllib.error
- from xml.etree import ElementTree as ET
- sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
- from auth import get_token, load_env # SDK does not support orgdborgsettings XML blob
-
- load_env()
- env_url = os.environ["DATAVERSE_URL"].rstrip("/")
- token = get_token()
-
- SETTING_NAME = "SearchAndCopilotIndexMode" # PascalCase, case-sensitive
- SETTING_VALUE = "0" # always a string in XML
-
- headers = {
- "Authorization": f"Bearer {token}",
- "Accept": "application/json",
- "Content-Type": "application/json",
- "OData-MaxVersion": "4.0",
- "OData-Version": "4.0",
- }
-
- # Fetch current XML
- req = urllib.request.Request(
- f"{env_url}/api/data/v9.2/organizations?$select=organizationid,orgdborgsettings",
- headers=headers,
- )
- with urllib.request.urlopen(req) as resp:
- org = json.loads(resp.read())["value"][0]
- org_id = org["organizationid"]
-
- root = ET.fromstring(org.get("orgdborgsettings", "<OrgSettings></OrgSettings>"))
-
- # Update existing or add new
- existing = root.find(SETTING_NAME)
- if existing is not None:
- print(f"Current {SETTING_NAME} = {existing.text}", flush=True)
- existing.text = SETTING_VALUE
- else:
- print(f"{SETTING_NAME} not set -- adding", flush=True)
- ET.SubElement(root, SETTING_NAME).text = SETTING_VALUE
-
- # PATCH back
- req = urllib.request.Request(
- f"{env_url}/api/data/v9.2/organizations({org_id})",
- data=json.dumps({"orgdborgsettings": ET.tostring(root, encoding="unicode")}).encode("utf-8"),
- headers=headers,
- method="PATCH",
- )
- try:
- with urllib.request.urlopen(req) as resp:
- print(f"SUCCESS: {SETTING_NAME} = {SETTING_VALUE} (HTTP {resp.status})", flush=True)
- except urllib.error.HTTPError as e:
- print(f"ERROR {e.code}: {e.read().decode()}", flush=True)
- ```
-
- **Remove an OrgDB setting:**
-
- ```python
- # After fetching and parsing the XML (same as above):
- existing = root.find(SETTING_NAME)
- if existing is not None:
- root.remove(existing)
- # PATCH back the XML without the element
- ```
-
- **Allowed OrgDB settings (17 keys — PascalCase, case-sensitive):**
-
- | Setting | Type | Values | PPAC label |
- |---|---|---|---|
- | `IsMCPEnabled` | bool | `true` / `false` | Allow MCP clients to interact with Dataverse MCP server |
- | `IsMCPPreviewEnabled` | bool | `true` / `false` | Advanced Settings (enable non-Copilot Studio MCP clients) |
- | `SearchAndCopilotIndexMode` | int | `0` Search Off / Copilot On; `1` Both On; `2` Both Off; `3` Search On / Copilot Off | Dataverse search + Search for records in Microsoft 365 apps (one key, two UI toggles — see truth table above) |
- | `IsLinkToFabricEnabled` | bool | `true` / `false` | Link Dataverse tables with Microsoft Fabric workspace |
- | `IsFabricVirtualTableEnabled` | bool | `true` / `false` | Define Dataverse virtual tables using Fabric OneLake data |
- | `ShowDataInM365Copilot` | bool | `true` / `false` | Allow data availability in Microsoft 365 Copilot |
- | `EnableWorkIQ` | bool | `true` / `false` | Turn on Dataverse intelligence (Work IQ) for agents |
- | `IsLockdownOfUnmanagedCustomizationEnabled` | bool | `true` / `false` | Block unmanaged customizations in environment |
- | `EnableSecurityOnAttachment` | bool | `true` / `false` | Enable security on Attachment entity |
- | `EnableTDSEndpoint` | bool | `true` / `false` | Enable TDS endpoint |
- | `AllowAccessToTDSEndpoint` | bool | `true` / `false` | Enable user level access control for TDS endpoint (requires TDS endpoint enabled first) |
- | `EnableOwnershipAcrossBusinessUnits` | bool | `true` / `false` | Record ownership across business units |
- | `CreateOnlyNonEmptyAddressRecordsForEligibleEntities` | bool | `true` / `false` | Disable empty address record creation (affects Account, Contact, Lead) |
- | `EnableDeleteAddressRecords` | bool | `true` / `false` | Enable deletion of address records |
- | `BlockDeleteManagedAttributeMap` | bool | `true` / `false` | Block deletion of OOB attribute maps |
- | `EnableSystemUserDelete` | bool | `true` / `false` | Enable delete disabled users |
- | `IsExcelToExistingTableWithAssistedMappingEnabled` | bool | `true` / `false` | Import Excel to existing table with AI-assisted mapping |
+ OrgDB settings live as PascalCase XML elements inside the `orgdborgsettings` column of the `organizations` entity. PAC CLI cannot read or write these — use raw Web API.
- Every other OrgDB key (`IsRetentionEnabled`, `IsArchivalEnabled`, `IsDVCopilotForTextDataEnabled`, `IsShadowLakeEnabled`, `IsCommandingModifiedOnEnabled`, `CanCreateApplicationStubUser`, `AllowRoleAssignmentOnDisabledUsers`, `EnableActivitiesFeatures`, `TDSListenerInitialized`, `AzureSynapseLinkIncrementalUpdateTimeInterval`, etc.) is **out of scope** — refuse and direct the user to the Power Platform admin center. Do NOT dump the whole `orgdborgsettings` XML to "discover" other settings for the user.
+ **Quick reference:** `GET /organizations?$select=organizationid,orgdborgsettings` → parse XML with `xml.etree.ElementTree` → modify or `SubElement` → `PATCH /organizations({id})` with the serialized XML.
- ---
+ For the read / update / remove Python patterns and the 17-key allowlist, see [`references/orgdb-settings.md`](references/orgdb-settings.md). Keys are case-sensitive (`IsMCPEnabled`, not `IsMcpEnabled`).
### Recycle Bin Configuration
- Recycle bin settings live in the `recyclebinconfigs` entity, NOT in `orgdborgsettings` XML. PAC CLI cannot manage these.
-
- **Well-known constant:** The organization entity metadata ID is `e1bd1119-6e9d-45a4-bc15-12051e65a0bd`. This is the `MetadataId` of the `organization` entity's *schema record* in `EntityDefinitions` (a product-level system constant baked into every Dataverse installation), not a tenant-level GUID — so it is identical across all environments and all tenants. Verified empirically across 5 environments. Do not re-query it per environment.
-
- ### Read Recycle Bin Status
-
- ```python
- import os, sys, json, urllib.request, urllib.parse
- sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
- from auth import get_token, load_env # SDK does not support recyclebinconfigs entity
-
- load_env()
- env_url = os.environ["DATAVERSE_URL"].rstrip("/")
- token = get_token()
-
- ORGANIZATION_ENTITY_ID = "e1bd1119-6e9d-45a4-bc15-12051e65a0bd"
-
- headers = {
- "Authorization": f"Bearer {token}",
- "Accept": "application/json",
- "Content-Type": "application/json",
- "OData-MaxVersion": "4.0",
- "OData-Version": "4.0",
- }
-
- # Fetch org-level config by extensionofrecordid (NOT by name)
- filter_q = urllib.parse.quote(f"_extensionofrecordid_value eq '{ORGANIZATION_ENTITY_ID}'")
- req = urllib.request.Request(
- f"{env_url}/api/data/v9.2/recyclebinconfigs?$filter={filter_q}&$select=recyclebinconfigid,statecode,statuscode,cleanupintervalindays",
- headers=headers,
- )
- with urllib.request.urlopen(req) as resp:
- records = json.loads(resp.read()).get("value", [])
-
- if records:
- config = records[0]
- enabled = config["statecode"] == 0
- cleanup = config["cleanupintervalindays"]
- print(f"Recycle bin: {'enabled' if enabled else 'disabled'}", flush=True)
- print(f"Cleanup interval: {cleanup} days ({'-1 means no auto-cleanup' if cleanup == -1 else ''})", flush=True)
- print(f"Config ID: {config['recyclebinconfigid']}", flush=True)
- else:
- print("Recycle bin: not configured (no org-level record)", flush=True)
- ```
-
- ### Critical: Always Send `isreadyforrecyclebin: true` on Enable
-
- **Every enable payload (POST or PATCH) must set `isreadyforrecyclebin: true`.**
-
- Without it, the platform defaults `isreadyforrecyclebin` to `false` (CREATE) or leaves it null (PATCH), which forces the platform into the **asynchronous** opt-in path — a `ProcessRecycleBin` background job is queued and your HTTP call returns success before any entity-level work happens. In that window, platform metadata operations (solution imports, attribute publish, async handlers) can race against the partial state and throw `EntityBinUpdateAction called for entity <x> which is not enabled for RecycleBin`. Sending `isreadyforrecyclebin: true` forces the synchronous, globally-locked opt-in path, which fans out to every entity inside one transaction.
-
- ### Critical: Disable via PATCH, Not DELETE
-
- **Disable with `PATCH statecode=1, statuscode=2, isreadyforrecyclebin=false`. Do not DELETE the org config record.**
-
- DELETE enqueues an async opt-out (when `RecycleBinOptOutOrgAsynchronously` is on) while leaving the org row marked Inactive and child entity rows still flagged `IsReadyForRecycleBin=true, IsDisabled=false`. Any platform operation that runs between your DELETE and your next enable will see "org is enabled" from the config cache, proceed to `RecycleBinConfigService.Update(<entity-config>)` synchronously, and throw when the DB-backed `IsRecycleBinEnabledForEntity` check disagrees. A PATCH-based disable takes the synchronous `OptOutOrganization` path under the customization lock, cleanly cascading to every entity.
-
- ### Wait for in-flight `ProcessRecycleBin` Jobs Between Toggles
-
- Every enable/disable queues a `ProcessRecycleBin` async operation (OperationType = `50`). Do NOT enable-then-disable-then-enable rapidly; the jobs share a dependency token and can interleave in ways that corrupt state. Before any second toggle, poll `AsyncOperation` until no `ProcessRecycleBin` row is `Queued` or `InProgress` for this org.
-
- ### Enable Recycle Bin
-
- Two cases depending on whether a config record already exists. Both send `isreadyforrecyclebin: true`.
-
- ```python
- # ... (same imports, headers, ORGANIZATION_ENTITY_ID, and fetch as above)
- # SDK does not support recyclebinconfigs entity
-
- CLEANUP_DAYS = 30 # default; -1 means records in recycle bin are never auto-purged
-
- # Pre-flight: wait for any in-flight ProcessRecycleBin async jobs to finish
- import time
- def wait_for_recyclebin_async_jobs(env_url, headers, timeout_s=120):
- # OperationType 50 = ProcessRecycleBin; StateCode 0=Ready/1=Suspended/2=Locked are all "not done"
- filter_q = urllib.parse.quote("operationtype eq 50 and statecode ne 3")
- url = f"{env_url}/api/data/v9.2/asyncoperations?$filter={filter_q}&$select=asyncoperationid,statecode,statuscode,name"
- deadline = time.time() + timeout_s
- while time.time() < deadline:
- req = urllib.request.Request(url, headers=headers)
- with urllib.request.urlopen(req) as resp:
- pending = json.loads(resp.read()).get("value", [])
- if not pending:
- return
- print(f" waiting on {len(pending)} ProcessRecycleBin job(s)...", flush=True)
- time.sleep(5)
- raise RuntimeError("Timed out waiting for pending ProcessRecycleBin async jobs")
-
- wait_for_recyclebin_async_jobs(env_url, headers)
-
- if not records:
- # Case 1: No config exists -- CREATE a new one
- # extensionofrecordid binds to the entities() metadata endpoint, NOT organizations()
- payload = {
- "extensionofrecordid@odata.bind": f"entities({ORGANIZATION_ENTITY_ID})",
- "extensionofrecordid@OData.Community.Display.V1.FormattedValue": "OrganizationId",
- "isreadyforrecyclebin": True, # MUST be true -- forces sync opt-in under the global lock
- "cleanupintervalindays": CLEANUP_DAYS,
- }
- req = urllib.request.Request(
- f"{env_url}/api/data/v9.2/recyclebinconfigs",
- data=json.dumps(payload).encode("utf-8"),
- headers=headers,
- method="POST",
- )
- with urllib.request.urlopen(req) as resp:
- print(f"SUCCESS: recycle bin enabled with {CLEANUP_DAYS} day cleanup (HTTP {resp.status})", flush=True)
- else:
- # Case 2: Config exists -- PATCH statecode/statuscode, cleanup interval, and isreadyforrecyclebin
- config_id = records[0]["recyclebinconfigid"]
- payload = {
- "cleanupintervalindays": CLEANUP_DAYS,
- "statecode": 0,
- "statuscode": 1,
- "isreadyforrecyclebin": True, # MUST be true -- without this, UpdateInternal routes through updateAsync
- }
- req = urllib.request.Request(
- f"{env_url}/api/data/v9.2/recyclebinconfigs({config_id})",
- data=json.dumps(payload).encode("utf-8"),
- headers=headers,
- method="PATCH",
- )
- with urllib.request.urlopen(req) as resp:
- print(f"SUCCESS: recycle bin enabled with {CLEANUP_DAYS} day cleanup (HTTP {resp.status})", flush=True)
-
- # Post-flight: drain the sync opt-in fan-out before returning control
- wait_for_recyclebin_async_jobs(env_url, headers)
- ```
-
- ### Disable Recycle Bin
-
- **Disable = PATCH `statecode=1, statuscode=2, isreadyforrecyclebin=false`.** This triggers the synchronous `OptOutOrganization` path which cascades cleanly to every entity config.
-
- ```python
- # ... (same fetch as above to get config_id)
- # SDK does not support recyclebinconfigs entity
-
- wait_for_recyclebin_async_jobs(env_url, headers) # drain first
-
- if records:
- config_id = records[0]["recyclebinconfigid"]
- payload = {
- "statecode": 1, # Inactive
- "statuscode": 2, # Inactive
- "isreadyforrecyclebin": False, # required to take the isOptOut branch in UpdateInternal
- }
- req = urllib.request.Request(
- f"{env_url}/api/data/v9.2/recyclebinconfigs({config_id})",
- data=json.dumps(payload).encode("utf-8"),
- headers=headers,
- method="PATCH",
- )
- with urllib.request.urlopen(req) as resp:
- print(f"SUCCESS: recycle bin disabled (HTTP {resp.status})", flush=True)
- else:
- print("Recycle bin is already disabled (no config record)", flush=True)
-
- wait_for_recyclebin_async_jobs(env_url, headers) # drain the opt-out fan-out
- ```
-
- **Do NOT use DELETE to disable.** Legacy guidance (including older Admin Center behavior) suggested DELETE, but DELETE enqueues an async opt-out and can leave per-entity configs orphaned — any platform metadata operation that runs before cleanup finishes will throw `EntityBinUpdateAction called for entity <x> which is not enabled for RecycleBin` on an unrelated entity.
-
- ### Key Fields on `recyclebinconfigs`
-
- | Field | Type | What it does |
- |---|---|---|
- | `statecode` | int | `0` = enabled (active), `1` = disabled (inactive) |
- | `statuscode` | int | `1` = enabled, `2` = disabled |
- | `cleanupintervalindays` | int | Auto-cleanup interval. `-1` = no auto-cleanup (default). `30` = purge after 30 days (max). Min: `1` |
- | `_extensionofrecordid_value` | guid | Entity metadata ID this config applies to. Org-level = `e1bd1119-6e9d-45a4-bc15-12051e65a0bd` |
+ Recycle bin settings live in the `recyclebinconfigs` entity (NOT `orgdborgsettings`). PAC CLI cannot manage them — use raw Web API.
- ### Important Notes
+ **Quick reference:** filter `recyclebinconfigs` by `_extensionofrecordid_value eq <ORG_ENTITY_METADATA_ID>` (the org-level metadata ID is a system constant: `e1bd1119-6e9d-45a4-bc15-12051e65a0bd`).
- - **Fetch by `_extensionofrecordid_value`**, not by `name`. The `name` field is unreliable for filtering.
- - **Create uses `entities()` binding** -- `extensionofrecordid@odata.bind: entities({id})`, NOT `organizations()`.
- - **Enable payloads MUST include `isreadyforrecyclebin: true`.** Without it, CREATE defaults to false and PATCH sends null — both force the async opt-in path and expose the org to cache-vs-DB races during platform metadata operations.
- - **Disable = PATCH `statecode=1, statuscode=2, isreadyforrecyclebin=false`**, not DELETE. DELETE enqueues an async opt-out and can leave per-entity configs orphaned.
- - **Drain `ProcessRecycleBin` async jobs between toggles.** Query `asyncoperations` for `operationtype eq 50 and statecode ne 3` before and after each enable/disable.
- - **Cleanup days**: default is `-1` (no auto-cleanup). Max is `30`. When the UI shows "30 days", the API stores `-1` internally (the platform applies a 30-day default).
- - Solution-managed configs (e.g., `msdyn_recurringsalesaction`) cannot be enabled/disabled via API.
- - **Per-table recycle bin toggles are out of scope.** PPAC only exposes the org-level on/off + cleanup days — if a user asks to enable/disable recycle bin for a specific table (e.g., "turn on recycle bin for `contact` only"), refuse with: *"Per-table recycle bin is out of scope for dv-admin. Use the Power Platform admin center."* The `recyclebinconfigs` entity does hold per-entity rows, but this skill only reads/writes the org-level row (filtered by the organization entity's MetadataId).
+ - **Enable:** PATCH `statecode=0, statuscode=1, isreadyforrecyclebin=true` (or POST a new config). `isreadyforrecyclebin: true` is required to force the synchronous opt-in path.
+ - **Disable:** PATCH `statecode=1, statuscode=2, isreadyforrecyclebin=false`. **Do NOT DELETE** — it enqueues async opt-out and orphans per-entity configs.
+ - **Drain in-flight `ProcessRecycleBin` async jobs** (`operationtype eq 50, statecode ne 3`) before any second toggle.
- ---
+ For the full Python lifecycle (read / enable / disable / async-drain helper), the cache-vs-DB race explanation, and the per-table out-of-scope rule, see [`references/recycle-bin.md`](references/recycle-bin.md).
### Settings-Definition Overrides (app/plan security roles)
- A small number of allowlisted toggles don't live on the `organization` entity or in `orgdborgsettings`. They're modeled as a join between two entities:
-
- - **`settingdefinition`** — defines the setting (uniquename, datatype, defaultvalue, description). Read-only; one row per known setting; identical across environments in the same build.
- - **`organizationsettings`** — holds per-org overrides. If no row exists for a given `settingdefinitionid`, the `defaultvalue` from `settingdefinition` applies.
-
- Allowlisted uniquenames (both `datatype=2` bool, stored as string `"true"`/`"false"`):
- - `PowerAppsAppLevelSecurityRolesEnabled` — Enable app level security roles for canvas apps
- - `PlanShareSecurityRolesEnabled` — Enable plan level security roles for plan designer
-
- **Read current value:**
-
- ```python
- import os, sys, json, urllib.request, urllib.parse
- sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
- from auth import get_token, load_env # SDK does not support settingdefinition/organizationsettings entities
-
- load_env()
- env_url = os.environ["DATAVERSE_URL"].rstrip("/")
- headers = {
- "Authorization": f"Bearer {get_token()}",
- "Accept": "application/json",
- "OData-MaxVersion": "4.0",
- "OData-Version": "4.0",
- "Content-Type": "application/json",
- }
-
- UNIQUENAME = "PowerAppsAppLevelSecurityRolesEnabled" # or PlanShareSecurityRolesEnabled
-
- q = urllib.parse.quote(f"uniquename eq '{UNIQUENAME}'")
- req = urllib.request.Request(
- f"{env_url}/api/data/v9.2/settingdefinitions?$filter={q}"
- f"&$select=settingdefinitionid,uniquename,defaultvalue,datatype",
- headers=headers,
- )
- with urllib.request.urlopen(req) as resp:
- defn = json.loads(resp.read())["value"][0]
-
- sd_id = defn["settingdefinitionid"]
- default = defn["defaultvalue"]
-
- q2 = urllib.parse.quote(f"_settingdefinitionid_value eq '{sd_id}'")
- req = urllib.request.Request(
- f"{env_url}/api/data/v9.2/organizationsettings?$filter={q2}&$select=organizationsettingid,value",
- headers=headers,
- )
- with urllib.request.urlopen(req) as resp:
- overrides = json.loads(resp.read())["value"]
-
- current = overrides[0]["value"] if overrides else default
- print(f"{UNIQUENAME} = {current} (default = {default}, override present: {bool(overrides)})", flush=True)
- ```
-
- **Write (idempotent CREATE-or-PATCH):**
-
- ```python
- # Continues from Read script above — reuses UNIQUENAME, sd_id, overrides, headers, env_url.
- # SDK does not support settingdefinition/organizationsettings entities.
- NEW_VALUE = "true" # bool-as-string; "true"/"false" (lowercase)
-
- if overrides:
- setting_id = overrides[0]["organizationsettingid"]
- req = urllib.request.Request(
- f"{env_url}/api/data/v9.2/organizationsettings({setting_id})",
- data=json.dumps({"value": NEW_VALUE}).encode("utf-8"),
- headers=headers,
- method="PATCH",
- )
- else:
- # No override exists — CREATE a new one
- payload = {
- "settingdefinitionid@odata.bind": f"settingdefinitions({sd_id})",
- "value": NEW_VALUE,
- }
- req = urllib.request.Request(
- f"{env_url}/api/data/v9.2/organizationsettings",
- data=json.dumps(payload).encode("utf-8"),
- headers=headers,
- method="POST",
- )
+ Two allowlisted toggles (`PowerAppsAppLevelSecurityRolesEnabled`, `PlanShareSecurityRolesEnabled`) live in a join: `settingdefinition` (defaults) + `organizationsettings` (overrides). PAC CLI doesn't manage these.
- with urllib.request.urlopen(req) as resp:
- print(f"SUCCESS: {UNIQUENAME} = {NEW_VALUE} (HTTP {resp.status})", flush=True)
- ```
+ **Quick reference:** look up the `settingdefinitionid` by `uniquename`, then either CREATE an `organizationsettings` row with `value` (string `"true"`/`"false"`) or PATCH the existing row. DELETE on the override row reverts to the default.
- **Notes:**
- - `datatype=2` means bool; other values exist for string/int but only bool toggles are in our allowlist today.
- - `value` is always a **string**, even for bool and int definitions — `"true"` not `True`.
- - The two allowlisted uniquenames are gated by ECS feature flags (`enablePowerAppsAppLevelSecurityRolesToggle`, `enablePlanShareSecurityRolesToggle`) in the PPAC UI, but the entities exist regardless — if the flag is off in an env, setting the override still takes effect.
- - `DELETE` on the override row reverts to the `settingdefinition.defaultvalue`.
+ For the read + idempotent CREATE/PATCH Python and the gating notes, see [`references/settings-overrides.md`](references/settings-overrides.md).
- ---
+ ## Operational confirmation rules
- ## Safety Rules
+ The four rules in the safety callout at the top of this file cover the irreversible / destructive cases. The rules below cover the non-destructive but still impactful operations:
- - **Always confirm** before scheduling a bulk delete — this is destructive
- - If `--fetchxml` is omitted, warn that ALL records in the table will be deleted
- - For system tables (`systemuser`, `businessunit`, `organization`), warn that deleting may break the environment
- - **Always confirm** before changing org settings that affect all users
- - For multi-environment updates, show the list of environments and get confirmation first
- - For OrgDB settings, warn that incorrect values can break environment features
- - For recycle bin, warn that reducing cleanup interval will permanently delete records sooner
- - For recycle bin enable/disable: always set `isreadyforrecyclebin` explicitly (true on enable, false on disable), use PATCH for disable (not DELETE), and drain any in-flight `ProcessRecycleBin` async jobs before toggling. Omitting these can produce `EntityBinUpdateAction called for entity <x> which is not enabled for RecycleBin` on unrelated platform operations.
+ - Confirm before changing org settings that affect all users.
+ - For multi-environment updates, show the list of target environments and get confirmation first.
+ - For OrgDB settings, warn that incorrect values can break environment features.
+ - For recycle bin cleanup interval changes, warn that reducing the interval permanently deletes recycled records sooner.
+ - For recycle bin enable/disable specifically: always set `isreadyforrecyclebin` explicitly (true on enable, false on disable), and drain any in-flight `ProcessRecycleBin` async jobs before any second toggle. Omitting these can produce `EntityBinUpdateAction called for entity <x> which is not enabled for RecycleBin` on unrelated platform operations.