Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsBlogPro
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Authors
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges
  • Chrome Extension
  • Skill Manager

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

ProTermsPrivacyRefunds
Back to skills

Dataverse Data

ASecurity

Record-level writes to Microsoft Dataverse tables with the Python SDK, the dataverse CLI, or MCP tools: create, update, delete, upsert with alternate keys, bulk CreateMultiple/UpsertMultiple, CSV import, multi-table foreign-key loads, DataFrame write-back, and schema-driven sample data. USE FOR: create Dataverse record, update record, delete record, upsert, alternate key upsert, bulk import, CSV import, CreateMultiple, UpsertMultiple, @odata.bind lookup, associate records, file column upload,...

3 stars
0 votes
0 copies
0 views
Added 10/5/2026
ai-agentsjavascriptpythongojavashellbashsqlnodeapidatabase

Works with

cliapimcp

Security Analysis

A92/100
mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 3 files and shows the line behind each finding

Scanned 10/5/2026

$npx -y skills add atc-net/atc-agentic-toolkit --skill dataverse-data --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Dataverse Data?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Dataverse Data
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/atc-net-dataverse-data/badge)](https://www.skillsdirectory.com/skills/atc-net-dataverse-data)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
Files
SKILL.md
---
name: dataverse-data
description: >
  Record-level writes to Microsoft Dataverse tables with the Python SDK, the dataverse CLI, or MCP tools:
  create, update, delete, upsert with alternate keys, bulk CreateMultiple/UpsertMultiple,
  CSV import, multi-table foreign-key loads, DataFrame write-back, and schema-driven sample data.
  USE FOR: create Dataverse record, update record, delete record, upsert, alternate key upsert,
  bulk import, CSV import, CreateMultiple, UpsertMultiple, @odata.bind lookup, associate records,
  file column upload, seed data, generate sample data, multi-table import with FK dependencies.
  DO NOT USE FOR: reading or analyzing records (use dataverse-query),
  tables, columns, relationships, alternate key definitions, forms, views (use dataverse-metadata),
  solution export or deployment (use dataverse-solution).
---

# Dataverse Data Writes

Create, update, delete, upsert, and bulk-import records in Microsoft Dataverse.

> **Python and the `dataverse` CLI only.** Do not script Dataverse with Node.js or JavaScript. If you are about to run `npm install` for a script or write a `.js` file, stop — see the hard rules in **dataverse-overview**.

## When to use

- Writing, modifying, or deleting individual records
- Bulk loads from CSV, Excel, or another database
- Idempotent re-runnable imports keyed on source-system IDs
- Seeding tables with realistic sample data for dev, demos, or tests

| Need | Use instead |
|---|---|
| Query, filter, aggregate, export records | **dataverse-query** |
| Tables, columns, relationships, alternate keys, forms, views | **dataverse-metadata** |
| Export or deploy solutions | **dataverse-solution** |

---

## Choose the write surface

| Volume / shape | Surface | Why |
|---|---|---|
| 1 record, one-liner | CLI `dataverse data create/update/upsert/delete` | No script, no workspace setup |
| 2–25 records, interactive | MCP `create_record` / `update_record` / `delete_record` | Up to 25 records per call |
| 25+ records, transformation, retries, CSV | SDK `client.records.*` | CreateMultiple/UpsertMultiple, paging, retry |
| Upsert by alternate key | CLI `data upsert` or SDK `UpsertItem` | MCP has no upsert tool |
| Associate / disassociate, file upload | CLI `data associate` / `data upload` | One-liner |

**CLI fast path:** if `dataverse auth who` shows an active profile, CLI writes work immediately — no `.env`, `auth.py`, or pip. SDK and bulk work still need the workspace set up by **dataverse-connect**.

**Scripted writes use the SDK, not hand-rolled HTTP.** The SDK carries auth, paging, and retry. Import `get_client` — not `get_token` plus `requests`. `get_token()` exists only for genuine gaps (global option sets, unbound actions), and even there prefer the managed `dataverse api` escape hatch.

### SDK coverage

| Supported | Not supported (use the alternative) |
|---|---|
| create, update, delete | Global option sets — see **dataverse-metadata** |
| Upsert with alternate keys | N:N association — CLI `dataverse data associate` or `POST .../<entity>(<id>)/<nav>/$ref` |
| CreateMultiple, UpdateMultiple, UpsertMultiple | `$apply` aggregation — use `client.query.fetchxml()` (**dataverse-query**) |
| File column uploads (chunked above 128 MB) | Unbound actions (`PublishXml`, `InstallSampleData`) — `dataverse api request` / `invoke` |
| Lookup reads inside a write workflow | DeleteMultiple, general OData `$batch` |
| Context manager with connection pooling | |

Forms and views (`systemform` / `savedquery`) are ordinary records — write them with `client.records.*` (see **dataverse-metadata**).

PyPI package: `PowerPlatform-Dataverse-Client` (GA 1.0.0). It is the only official package — never install `dataverse-api` or other look-alikes.

---

## CLI write commands

```bash
# Create (--table is the EntitySet name)
dataverse data create --table accounts --data '{"name":"Contoso"}' --return --json

# Update by GUID
dataverse data update --table accounts --id <guid> --data '{"name":"Contoso (updated)"}' --json

# Upsert by alternate key (idempotent)
dataverse data upsert --table accounts --key "accountnumber='ACC-001'" --data '{"name":"Contoso Ltd"}' --json

# Delete (--no-confirm skips the prompt)
dataverse data delete --table accounts --id <guid> --no-confirm

# Associate (N:N or lookup)
dataverse data associate --table accounts --id <guid> --relationship contact_customer_accounts --related contacts --related-id <contact-guid>

# Disassociate (N:N: pass --related-id; clearing a lookup: omit it)
dataverse data disassociate --table accounts --id <guid> --relationship contact_customer_accounts --related-id <contact-guid>

# Upload to a file column (--table is the LogicalName here, not the EntitySet)
dataverse data upload --table account --id <guid> --column new_document --file report.pdf

# Describe schema (attributes, relationships, actions)
dataverse data describe --table account --include all --json

# Invoke a custom API (find names with 'dataverse api list')
dataverse api invoke <CustomApiName> --target dataverse --param Input=value

# Raw escape hatch for built-in actions (--target is required)
dataverse api request --target dataverse --path "/api/data/v9.2/WhoAmI"
```

---

## SDK setup

```python
import os, sys
sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
from auth import get_client

client = get_client()
```

`get_client()` loads `.env`, resolves the environment URL, and handles auth (see `scripts/auth.py`, installed by **dataverse-connect**). For scripts that run to completion, wrap the client in a `with` block for connection cleanup; in notebooks the bare client is fine.

## Field name casing

Wrong casing is the most common cause of HTTP 400.

| Property type | Convention | Example | Used in |
|---|---|---|---|
| Structural (columns) | LogicalName, always lowercase | `new_name` | Payload keys |
| Navigation (lookups) | Navigation property name, case-sensitive per `$metadata` | `new_AccountId` | `@odata.bind` keys |

The SDK lowercases structural keys but preserves `@odata.bind` key casing.

---

## Single-record operations

```python
# Create
guid = client.records.create("new_ticket", {
    "new_name": "Ticket 001",
    "new_priority": 100000002,                          # choice: integer value, never the label
    "new_AccountId@odata.bind": "/accounts(<account-guid>)",
})

# Update
client.records.update("new_ticket", guid, {"new_status": 100000001})

# Delete
client.records.delete("new_ticket", guid)
```

### `@odata.bind` rules

- Key is the navigation property name plus `@odata.bind`; value is `"/<EntitySetName>(<guid>)"`.
- EntitySetName is not always logical name + `s` (`country` → `countries`). Look it up in `EntityDefinitions?$select=LogicalName,EntitySetName`.
- Just created the lookup column? Wait 5–10 seconds before inserting — metadata propagation causes "Invalid property" errors.

| Lookup | Correct key | Wrong |
|---|---|---|
| Custom `new_AccountId` | `new_AccountId@odata.bind` | `new_accountid@odata.bind` |
| Polymorphic `customerid` | `customerid_account@odata.bind` | `customerid@odata.bind` |
| `parentcustomerid` | `parentcustomerid_account@odata.bind` | `_parentcustomerid_value@odata.bind` |

Find the navigation property name: after creating a lookup with the SDK, read `result.lookup_schema_name`. For existing tables:

```text
GET /api/data/v9.2/EntityDefinitions(LogicalName='<entity>')/ManyToOneRelationships?$select=ReferencingEntityNavigationPropertyName,ReferencedEntity
```

---

## Bulk operations

```python
# Bulk create (CreateMultiple)
records = [{"new_name": f"Ticket {i}", "new_priority": 100000000} for i in range(500)]
guids = client.records.create("new_ticket", records)

# Broadcast one change to many records (UpdateMultiple)
client.records.update("new_ticket", [id1, id2, id3], {"new_status": 100000001})
```

**The SDK does not chunk.** It sends the whole list in one POST. There is no fixed record-count limit; the limits are payload size and the 120 s POST timeout. Chunk in your script: start at 1,000, double on success up to 4,000, halve on 413/500/timeout and cap at the last good size. Narrow tables tolerate larger chunks. The adaptive `bulk_upsert` / `bulk_create` helpers are in [references/multi-table-fk-import.md](references/multi-table-fk-import.md).

### DataFrame write-back

```python
client.dataframe.update("opportunity", df_updates, id_column="opportunityid")  # df must include the PK
guids = client.dataframe.create("opportunity", df_new_records)                # returns a Series of GUIDs
```

DataFrame write-back supports create and update only — no upsert. Reads into DataFrames are in **dataverse-query**.

---

## Upsert with alternate keys

Idempotent — re-running never creates duplicates. The alternate key must already exist on the table (**dataverse-metadata**).

```python
from PowerPlatform.Dataverse.models.upsert import UpsertItem

client.records.upsert("account", [
    UpsertItem(alternate_key={"accountnumber": "ACC-001"},
               record={"name": "Contoso Ltd", "description": "Primary account"}),
    UpsertItem(alternate_key={"accountnumber": "ACC-002"},
               record={"name": "Fabrikam Inc"}),
])
```

**Never repeat alternate key columns in `record`.** Single upsert tolerates it; `UpsertMultiple` fails with "An unexpected error occurred".

---

## CSV import

Use `create()` only for one-shot loads. Anything that might be re-run should use `UpsertItem` (see above and the multi-table reference).

```python
import csv, os, sys
sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
from auth import get_client

client = get_client()

with open("data/customers.csv", newline="", encoding="utf-8") as f:
    rows = list(csv.DictReader(f))

records = [{"new_name": r["name"], "new_email": r["email"]} for r in rows]

chunk_size = 1000  # one POST per chunk; raise for narrow tables
for i in range(0, len(records), chunk_size):
    guids = client.records.create("new_customer", records[i:i + chunk_size])
    print(f"Imported {i + len(guids)}/{len(records)}", flush=True)
```

### Resolve lookups before import

When the source has a business key (e.g. email) and Dataverse needs a GUID, build a map first:

```python
email_to_guid = {
    r["new_email"]: r["new_customerid"]
    for r in client.records.list("new_customer", select=["new_customerid", "new_email"])
}

records = []
for row in rows:
    customer_guid = email_to_guid.get(row["customer_email"])
    if not customer_guid:
        print(f"Skipping row, unknown email: {row['customer_email']}", flush=True)
        continue
    records.append({
        "new_channel": row["channel"],
        "new_CustomerId@odata.bind": f"/new_customers({customer_guid})",  # verify EntitySetName
    })

guids = client.records.create("new_interaction", records)
```

### Discover required fields on system tables

Before bulk-creating into `account`, `contact`, `opportunity`, etc.:

1. Create one test record with your minimal payload.
2. On `HttpError` 400 the message names the missing required field. Some requirements are plugin-enforced and not visible in `describe`.
3. Delete the test record, then run the bulk load.

---

## Multi-table import with FK dependencies

Load in dependency order with `UpsertItem` and alternate keys on source IDs:

1. Create tables with source-ID columns, alternate keys, and lookups (**dataverse-metadata**).
2. Import Level 0 tables (no FKs) in parallel with `ThreadPoolExecutor`, one worker per table.
3. Query back to build source-ID → GUID maps (upsert does not return GUIDs).
4. Repeat per level — Level N binds to Level N-1 maps via `@odata.bind`.

Invariants:

- Parallel across tables at the same level; sequential between levels; **sequential chunks within a table** (concurrent writes to one table deadlock, SQL error 1205).
- Alternate key columns never appear in the record body.
- Catch failures per table inside the executor so one table cannot kill the others.
- Start `chunk_size=1000` and let the helper adapt.

Full helpers, composite keys, verification, and the create-only variant: [references/multi-table-fk-import.md](references/multi-table-fk-import.md).

---

## Sample data generation

Generate records inline from the table schema — table-agnostic and PII-safe (`@example.com` emails, `555-01xx` phones).

Flow: confirm environment, table, and count (default 5) → read required columns with `client.tables.list_columns(TABLE, filter="AttributeOf eq null")` → pick a generator per `AttributeType` → `client.records.create()` (list form for 10+ records).

- Skip Lookup, Uniqueidentifier, State, Status, Owner, and Customer columns unless the user supplies values.
- `DisplayName.UserLocalizedLabel` can be null — dereference safely.

Confirm before running, with a concrete plan instead of an open question:

- Avoid: "Which environment should I target? Please provide the Dataverse URL."
- Prefer: "I'll generate 20 `contact` records with CreateMultiple, `@example.com` emails and `555-01xx` phones, against the active `pac auth list` environment. Confirm, or name a different environment."

For a custom table with unknown schema, say you will query `EntityDefinitions` for required columns first, then confirm the count.

Template and safety rules: [references/sample-data-generation.md](references/sample-data-generation.md).

---

## Error handling

```python
from PowerPlatform.Dataverse.core.errors import HttpError

try:
    guid = client.records.create("new_ticket", {"new_name": "Test"})
except HttpError as e:
    print(f"Status {e.status_code}: {e.message}")
    if e.details:
        print(f"Details: {e.details}")
```

| Status | Likely cause |
|---|---|
| 400 | Wrong field name or casing, bad `@odata.bind`, missing required field, alternate key column in upsert body |
| 403 | Missing security role privileges |
| 404 | Table or record not found |
| 413 / 500 on bulk | Chunk too large — halve it |
| 429 | Throttled; the SDK retries — reduce chunk size if it persists |

## Safety rules

- Confirm the target environment before any write, bulk load, or delete.
- Prefer upsert over create for anything that may be re-run.
- Never parallelize chunks within a single table.
- Log and skip rows whose lookup cannot be resolved; do not guess GUIDs.
- Always query the live environment to verify results — not local copies of the source data.

## Windows scripting

- ASCII only in `.py` files — curly quotes and em dashes cause `SyntaxError` on Windows.
- Do not use `python -c` for multi-line code; write a `.py` file.
- Generate GUIDs in Python (`str(uuid.uuid4())`), not via shell substitution.
- Use `flush=True` on progress prints.

## References

| Reference | When to load |
|---|---|
| [references/multi-table-fk-import.md](references/multi-table-fk-import.md) | Importing several related tables, adaptive chunking helpers, composite keys, post-import count verification |
| [references/sample-data-generation.md](references/sample-data-generation.md) | Generating sample/seed records for any table |

Attribution

atc-netatc-net
View sourceSee grades on GitHubMore from atc-net →
SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Related Skills

Caveman

Terse caveman voice: answer first, fluff gone, every technical fact kept. Use for /caveman, "caveman mode", "talk like caveman", "be brief", "less tokens". Stays on until "stop caveman" or "normal mode".

1100021 votes

Hyperplan

Adversarial multi-agent planning skill. Self-orchestrates 5 hostile category members (unspecified-low, unspecified-high, deep, ultrabrain, artistry) via team-mode for ruthless cross-critique debate, distills only the defensible insights, then MANDATORILY hands the distilled insight bundle to the `plan` agent for executable plan formalization. Use when planning needs maximum rigor and surfacing of weak assumptions, blind spots, and over-engineering. Triggers: 'hyperplan', 'hpp', '/hyperplan', ...

698621 votes

Writing Skills

Create and manage Claude Code skills in HASH repository following Anthropic best practices. Use when creating new skills, modifying skill-rules.json, understanding trigger patterns, working with hooks, debugging skill activation, or implementing progressive disclosure. Covers skill structure, YAML frontmatter, trigger types (keywords, intent patterns), UserPromptSubmit hook, and the 500-line rule. Includes validation and debugging with SKILL_DEBUG. Examples include rust-error-stack, cargo-dep...

3931 votes

Mcp Code Execution

Routes multi-tool workflows through MCP servers for large datasets and pipelines. Use when Bash tool overhead is limiting throughput on data-heavy tasks.

3421 votes

catchup

Recovers the conversation and failed tool calls of a previous Codex, Amp, Claude Code, Antigravity, Cline, Copilot CLI, Cursor, DeepSeek Harness, Grok Build, Kimi, OpenCode, Pi Agent, or ZCode session. Use when the user says "catch up", "what did the last session do", "get me up to speed", "I switched agents", asks to recover/summarize a previous session before continuing, or asks to diagnose or report a catchup failure. Do NOT use for the current conversation, git history, or any non-agent log.

741 votes
View all in ai-agents →