- name
- add-sample-data
- description
- Use when the user wants to seed Dataverse tables with realistic sample records so a freshly-scaffolded code app shows real-looking data on first launch. Generates contextually appropriate rows from each table's schema and inserts them in dependency order. Mirrors microsoft/power-platform-skills/power-pages/add-sample-data, adapted for mobile apps.
- user-invocable
- true
- allowed-tools
- Read, Edit, Write, Grep, Glob, Bash, AskUserQuestion
- model
- sonnet
> **Plugin check**: Run `node "${PLUGIN_ROOT}/scripts/check-version.js"` - if it outputs a message, show it to the user before proceeding.
**📋 Shared instructions: [shared-instructions.md](${PLUGIN_ROOT}/shared/shared-instructions.md)** — read first.
# Add Sample Data
Populate Dataverse tables with realistic sample records so a freshly-scaffolded code app shows real-looking data on first launch. Generates rows from each table's schema and inserts them in dependency order. Use after `/add-dataverse` (or `/setup-datamodel`) has created the tables.
## Core principles
- **Coverage over volume — every table in the manifest gets seeded.** The #1 failure mode of a freshly-scaffolded code app is a home / dashboard / list screen that renders an empty state on first launch because its source table has zero rows. An empty downstream table is **worse than a 3-row table.** Default to minimal-but-complete: small counts everywhere, no table left empty. Volume is a secondary knob — coverage is the contract.
- **Insertion order matters.** Parent / referenced tables must be inserted before child / referencing tables so lookup IDs are available.
- **Contextual data, not Lorem Ipsum.** Generate values that match column names + types. A `cr3e9_sitename` column in an inspection app gets "Westside Construction Site", not "Sample Name 1".
- **Scenario-aware rows.** Read `native-app-plan.md`, especially `### Shared Conventions` and per-screen `Operational pattern` values defined in [screen-templates.md](${PLUGIN_ROOT}/shared/references/screen-templates.md). Seed rows should exercise the app's actual workflow: statuses, dates, relationships, priority/severity, media metadata, and edge cases that make the planned first viewport light up.
- **Fail gracefully.** On insertion failure, log the error and continue with remaining records — never auto-rollback. The user can re-run after fixing the issue.
- **Idempotent re-runs.** If a previous run partially completed, the second run reads `memory-bank.md`'s seeded-data table and skips records already inserted.
- **Solution-scoped inserts.** Always pass `--solution <uniqueName>` so records land in our solution, not the default.
## Workflow
1. Verify project + auth → 2. Discover tables → 3. Select tables + count → 4. Generate + preview → 5. Insert → 6. Summary
## Prototype Seed Reuse
`--from-seed` is used by `/prototype-to-real-app` after a mock prototype is converted to Dataverse. In this mode, prefer existing prototype seed files before generating new rows:
```text
src/generated/services/*/*.seed.json
src/generated/services/*.seed.json
```
Map seed objects to Dataverse payloads using `.datamodel-manifest.json`:
- Keep values only for real manifest columns.
- Translate lookup references into exact `<schemaName>@odata.bind` keys from the manifest.
- Keep picklist integers from the manifest; do not invent values from labels.
- Skip local-only prototype fields that have no Dataverse column.
- Preserve dependency-tier insertion order.
If a seed file cannot be mapped safely, fall back to generated contextual sample rows for that table and record `DONE_WITH_CONCERNS` in the summary. `--from-seed` is a preference, not permission to insert malformed data.
---
### Step 1 — Verify project & auth
```bash
test -f power.config.json && test -f app.config.js
node "${PLUGIN_ROOT}/scripts/resolve-environment.js" "$(node -e \"console.log(require('./power.config.json').environmentId)\")"
```
Capture the **environment URL** for subsequent script calls. If resolution fails, instruct `az login --tenant <env-tenant>` or ask for the environment URL directly, then stop.
Verify Azure CLI auth (the script needs an Azure CLI token):
```bash
az account show --query "user.name" -o tsv
```
If empty, instruct `az login` and stop.
### Step 2 — Discover tables
**Telemetry checkpoint: `discover_dataverse_tables`**
#### Step 2a — Path A: read `.datamodel-manifest.json` (preferred)
```bash
test -f .datamodel-manifest.json
```
If present, parse the JSON. It already contains `logicalName`, `displayName`, `status` (`new` / `extended` / `reused`), and `columns` for every table the project uses. **This is the preferred path** — fast, no API calls.
```bash
cat .datamodel-manifest.json | jq '.tables[] | { logicalName, displayName, columnCount: (.columns | length) }'
```
Skip Step 2b.
#### Step 2b — Path B: query OData (fallback)
If `.datamodel-manifest.json` is missing, discover custom tables via the script:
```bash
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> GET \
"EntityDefinitions?\$select=LogicalName,DisplayName,EntitySetName&\$filter=IsCustomEntity eq true"
```
For each table the project uses, fetch its custom columns:
```bash
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> GET \
"EntityDefinitions(LogicalName='<table>')/Attributes?\$select=LogicalName,DisplayName,AttributeType,RequiredLevel&\$filter=IsCustomAttribute eq true"
```
Build the same `{ logicalName, displayName, columns: [...] }` shape the manifest provides.
### Step 3 — Select tables + count
All tables from the manifest are evaluated — including reused ones — because a mobile app that surfaces data from a shared table still needs rows to render on first launch. The only exception is standard system tables (e.g. `contact`, `account`, `systemuser`) where seeding is risky in shared production environments.
**Pre-seeding row-count check (HARD — runs for every table before generating any rows):**
For each table, query its current record count using the entity set name from the manifest (or derive it by appending `s` to the logical name as a fallback):
```bash
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> GET \
"<entitySetName>?\$top=5&\$select=<primaryKeyColumn>"
```
Count the rows returned in the `value` array.
| Existing record count | Action |
|---|---|
| **≥5** | **Skip this table entirely.** Log: `↷ <table> (≥5 records exist, skipping)`. Do not generate or insert any rows. |
| **<5** | Seed enough new rows to reach the per-class target count. If some records already exist (e.g. 2), generate only the gap (e.g. 3 more to reach 5). |
If all tables already have ≥5 records, print `→ All tables already have ≥5 records. Nothing to seed.` and stop.
**Per-table count by class** (classify each table from manifest signals before generating; this beats a uniform `5` because reference tables don't need volume and transactional tables need state spread):
| Class | Heuristic | Default count | Rationale |
|---|---|---|---|
| **Reference** | No `status` / `state` column; columns are descriptive (name, address, phone). Often Tier 0. | **3** | Stores, customers, sites, products. Small stable set. |
| **Junction** | Two-or-more lookups, no other meaningful columns. Often Tier 1. | **3 × parent count, capped at 9** | Store-Assignment, Project-User. Needs to span the join. |
| **Transactional** | Has a `status` / `state` / `phase` choice column AND a date column (`createdon`, `submittedat`, `completedat`). | **5** | Audits, inspections, orders, tickets. Need state mix to make tiles light up. |
| **Detail / line-item** | Lookup back to a transactional parent, no own state column. | **2-3 per parent** | Audit zones, order line items, inspection findings. |
| **Issue / finding** | Lookup back to a transactional parent + has `status` (Open / Resolved) AND severity (Critical / Moderate / Minor). | **3-4 per transactional parent** | Issues, defects, observations. Mix severities + statuses (see Step 4a). |
| **Evidence / attachment** | Has a File / Image column + lookup to issue/inspection. | **1 per ~30% of parents** | Photo evidence, document uploads. Seed metadata rows by default; seed file/image bytes only when `brand/media-policy.md` or the user's request says sample media is needed. |
| **Log / event / audit-trail** | Append-only with `eventtype` enum + timestamp + actor lookup. | **2 per transactional parent** | Audit log events, activity stream. Mix at least 2 event types per parent. |
| **Override / approval** | Lookup to transactional parent + status (Pending / Approved / Rejected). | **1 per ~20% of parents** | Override requests, approval queue. At least 1 row in `Pending` so queue tab shows content. |
Counts are intentionally minimal. Goal: every screen has SOMETHING to render, not a demo dataset.
Print a one-line summary and continue:
> `→ Seeding <total> records into <N> tables (coverage-first; counts auto-tuned per class).`
#### Step 3b — Determine insertion order
For the selected tables, build a dependency graph from lookup columns:
1. Tables with no lookups out → Tier 0 (insert first)
2. Tables with lookups only to Tier 0 → Tier 1
3. Continue until all selected tables are tiered
If a selected table references an UNSELECTED parent, ask the user whether to add the parent to the selection or skip the lookup field. Don't silently insert null lookups.
### Step 4 — Generate sample data + preview
**Telemetry checkpoint: `generate_and_review_sample_records`**
#### Step 4a — Generate contextual rows
For each selected table, generate N rows. Match values to column names + types:
| Column type | Generation approach |
|---|---|
| **String** | Match the column name's semantic. `*name`, `*title` → realistic names from the requirements brief context. `*email` → `firstname.lastname@example.com`. `*phone` → `(555) 123-NNNN`. `*address` → realistic street + city. Otherwise: short context-appropriate text. |
| **Memo (multi-line text)** | 1-3 sentences relevant to the column name (e.g. `*notes`, `*description`). |
| **Integer / Decimal / Currency** | Reasonable range based on column name. `*amount`, `*price` → realistic dollars. `*count`, `*quantity` → small integers (1-100). |
| **DateTime / DateOnly** | Recent ISO dates spanning past 30 days to next 14 days. Vary across rows. |
| **Boolean** | Mix true/false (~70/30 favoring true for `is_active` style names). |
| **Choice (Picklist)** | **Query options first** (Step 4b), then pick from valid integer values. |
| **MultiSelect Choice** | Pick 1-3 valid values per row from the option set. |
| **Lookup** | Reference a record from the parent table that was (or will be) inserted in this run. Track parent GUIDs from Step 5's POST responses. |
| **Image / File** | Default: skip — leave null. If media seeding is enabled and the column is business data (product image, inspection evidence, NC proof), use generated/synthetic local files from `assets/sample-*` and record provenance. Never upload decorative UI hero assets to Dataverse. |
**Media seeding policy (business data only, only if needed):**
- Default: do not seed binary media. Seed metadata rows and leave Image/File columns null unless the screen plan or user request requires visible sample media.
- Seed Dataverse images/files only when the image belongs to a record users inspect in list/detail screens: product photos, evidence, attachments, signatures, issue proof. Do NOT seed Home hero, splash, app icon, empty-state art, or decorative detail backgrounds.
- Prefer generated/synthetic assets with no logos, no real product labels, no faces, no watermarks, and no competitor branding. If the user supplies approved assets, use those and record their source.
- CDN URLs are valid only for explicit URL/Text columns (e.g. `imageurl`, `photourl`). Do not put CDN URLs into File/Image columns.
- Dataverse Image columns receive base64 in the row payload or generated service shape. Dataverse File columns require a second upload step after the metadata row exists.
- For product/channel apps, product images are core sample data when product list/detail screens are visual, but they are capped. Generate and upload only a representative subset; use local placeholders or null image fields for the rest.
- Maintain `assets/images/asset-manifest.json` or `assets/sample-media/asset-manifest.json` with file, purpose, source/license, and safety notes.
**Media volume limits (HARD):**
- Product/catalog tables: upload images for at most **min(6, record count)** records by default. If records are category-based, choose 1-2 per category until the cap is reached. All remaining records rely on local placeholder thumbnails or empty-state fallback.
- Evidence/attachment tables: upload sample files for at most **30% of parent records**, capped at **5 files total per table**. Metadata rows can still exist without file bytes.
- User/avatar/equipment/site images: upload at most **3 per table** unless the user explicitly asks for a larger visual demo.
- Never create more than **10 generated media files total** in one `/add-sample-data` run without explicit user approval.
- If the calculated sample row count exceeds the media cap, prefer diverse coverage over volume: one hero product, one secondary product, one edge/status example, then placeholders.
**How sample images are inserted:**
1. Generate the normal record body first (name, lookup fields, status, etc.).
2. For **Image columns**, attach a compact base64 payload in the create/PATCH body only if the generated model/service expects base64 for that column. Keep the image small enough for mobile thumbnails.
3. For **File columns**, do NOT put bytes/base64/URLs in the create body. Insert the metadata row first, capture the GUID from Step 5's `BATCH-RECORDS` result, then upload the generated file to `(recordId, columnName)` in Step 5d.
4. Stop once the media cap is reached. Remaining records keep null Image/File columns and use local placeholders in the app UI.
5. If no upload helper exists for File columns, leave the column null and report `sample media skipped — upload helper missing`. Never fake File/Image data with a URL string.
**Pull context from the requirements brief.** The user described what the app does (e.g. "HVAC inspection app for field technicians"); use that to flavor the data — sites named after streets typical for the user's industry, statuses in the right vocabulary. Generic Lorem Ipsum is the failure mode.
**Per-parent fanout floor (HARD).** For every child table in Tier K+, generate AT LEAST 1 row per parent row from Tier K-1 unless the relationship is explicitly optional (`RequiredLevel: None` in the manifest AND the column name doesn't imply 1-to-many like `*audit*`, `*inspection*`, `*order*`). Without this floor, random lookup distribution leaves some parents with zero children and the parent's detail screen renders empty. Concrete rule: if generating `audit_zones` and there are 5 audits, generate AT LEAST 5 zones (one per audit), then add 0-2 more per audit until you hit the per-class count target. Never the reverse — never generate `N` total and let chance decide which parent each row picks.
**State / status distribution (HARD for transactional, issue, override classes).** If a table has a `status` / `state` / `phase` / `severity` / `priority` choice column, **distribute rows across at least 2 distinct values** — never all-`Open`, never all-`InProgress`. Concrete rules:
- **Transactional** (`audit`, `inspection`, `order`): mix at least 3 lifecycle states from the option set if available — typically 1 row in an early state (`Draft` / `InProgress`), 1 in a mid state (`Submitted` / `PendingReview`), and the rest in a terminal state (`Signed` / `Completed` / `Closed`). If only 2 states exist, split ~60/40.
- **Issue / finding**: mix severity AND status independently. If 5 issues across 3 severities × 2 statuses, aim for ≥1 of each severity AND ≥1 `Open` AND ≥1 `Resolved`.
- **Override / approval**: at least 1 row in the queue-driving state (`Pending`) so the queue/inbox tab shows content; remainder distributed across `Approved` / `Rejected`.
- **Log / event**: at least 2 distinct `eventtype` values per parent (e.g. `Created` + `StatusChanged`).
**Date distribution (transactional / log only).** If the table has a `createdon` / `submittedat` / `completedat` / `eventtimestamp` column, spread rows across **today + last 14 days** (not all today, not all 30 days ago). Distribution: ~30% today/yesterday (drives "Recent activity" tiles), ~50% last 7 days, ~20% 8-14 days. Reference and detail tables can use any reasonable date — only transactional/log need temporal spread.
**Scenario archetype edge coverage (HARD when detectable).** Use the selected Power Apps scenario archetype to ensure at least one row exercises each critical state the app UI promises:
- Field inspection / audit: one blocked or evidence-missing audit, one in-progress audit, one completed/signed audit.
- Asset maintenance: one overdue/high-priority work order, one awaiting-parts order, one completed order.
- Inventory scan-first: one variance, one zero/low-stock item, one resolved count/transfer.
- Approvals: one pending item in the approval inbox, one approved, one rejected/changes-requested.
- CRM: one at-risk relationship, one upcoming follow-up, one healthy/won relationship.
- Retail catalog/order: one visual product, one low-stock/backordered example, one order with multiple lines.
- Case management: one escalated/SLA-risk case, one waiting-on-customer case, one resolved case.
- Onboarding/training: one overdue required module, one in-progress module, one completed/certified module.
- Expense/request intake: one missing-receipt draft, one pending approval, one approved/paid request.
- Health/wellness/care: one due-today care task, one missed/follow-up task, one completed/on-track goal.
#### Step 4b — Discover Choice options
For every choice column in the selected tables, query its option set before generating rows:
```bash
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> GET \
Auf GitHub ansehen