[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M699MKRGV2V44WCAYAYP4QEC":3,"skill-same:01M699MKRGV2V44WCAYAYP4QEC":80},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":44,"linkOnly":45,"markdownContent":46,"markdownBody":47,"frontMatter":48,"refName":51,"commitSha":52,"skillDirectory":53,"forksCount":54,"repositoryHtmlUrl":55,"repositoryHomepageUrl":56,"ownerHtmlUrl":57,"ownerBlogUrl":58,"ownerDescription":59,"updatedAt":60,"install":61,"resources":73},"01M699MKRGV2V44WCAYAYP4QEC","add-dataverse",null,"Use when the user wants to add Dataverse tables (existing or new) to a Power Apps mobile app, extend an existing Dataverse table with new columns, or apply an approved data model plan.","microsoft\u002Fpower-platform-skills","microsoft","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F6154722?v=4","01M698VYDXZ6JW5WH3CRV650R8","Microsoft","01M699JVYJFB75D9JM70BN4H0Q",1149,"MIT","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fmobile-apps\u002Fskills\u002Fadd-dataverse\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",[24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43],"add dataverse","add","dataverse","power","apply","Existing","tables","opus","apps","app","mobile","mobile app","use","user","edit","write","read","table","plan","power apps","2026-11-10 10:12:30",false,"---\nname: add-dataverse\ndescription: Use when the user wants to add Dataverse tables (existing or new) to a Power Apps mobile app, extend an existing Dataverse table with new columns, or apply an approved data model plan.\nuser-invocable: true\nallowed-tools: Read, Edit, Write, Grep, Glob, Bash, AskUserQuestion, EnterPlanMode, ExitPlanMode, Task, Skill\nmodel: opus\n---\n\n> **Plugin check**: Run `node \"${PLUGIN_ROOT}\u002Fscripts\u002Fcheck-version.js\"` - if it outputs a message, show it to the user before proceeding.\n\n**? Shared instructions: [shared-instructions.md](${PLUGIN_ROOT}\u002Fshared\u002Fshared-instructions.md)** — read first.\n\n# Add Dataverse\n\n**App root:** before any project read or command, execute\n[app-working-directory.md](${PLUGIN_ROOT}\u002Fshared\u002Freferences\u002Fapp-working-directory.md).\nUse its resolved absolute `working_dir` for every shell call and file tool,\nincluding referenced commands and delegated skills; never inherit a prior `cd`.\n\n**Entry routing:** use the shared [App feature entry points](..\u002F..\u002Fshared\u002Fshared-instructions.md#app-feature-entry-points)\npreflight before the workflow below.\n\n**Invocation scope:** follow [Data-source invocation scope](..\u002F..\u002Fshared\u002Fshared-instructions.md#data-source-invocation-scope)\nbefore the workflow below. Standalone calls use this skill's own data-delta\napproval and verification; do not start a full-app plan or screen workflow.\n\nAn existing plan alone does not mean it includes or approves the new request;\nnever replay the old Data Model instead of resolving the requested delta.\n\n**Removal branch:** after resolving invocation scope, `--remove` or an approved app-binding\nremoval executes\n[data-source-removal.md](..\u002F..\u002Fshared\u002Freferences\u002Fdata-source-removal.md), then\nreturns without entering Steps 1-9. Removing a plan row is not implemented by\nre-running table creation or by deleting Dataverse metadata. For a mixed edit,\nthe owner adds\u002Frefreshes first and invokes removal separately after consumer edits.\n\n**Refresh branch:** after resolving invocation scope, `--refresh` or an approved service-only\nrefresh executes [Refresh a retained source](..\u002F..\u002Fshared\u002Freferences\u002Fdata-source-removal.md#refresh-a-retained-source)\nand returns before Steps 1-9. Preserve the exact `--data-source-name` and approved\nbinding identity. Do not replay schema writes, publish, or run `add-data-source`.\n\nTwo paths:\n\n- **Existing tables only** — resolve the current scope in Steps 1–2, then skip schema writes and use Step 6 to add only missing approved bindings or refresh approved retained sources.\n- **New \u002F extended tables** — full workflow with Web API mutations in dependency order\n\n## Workflow\n\n1. Verify project & auth → 2. Resolve plan\u002Foperation manifest → 3. Setup Dataverse Web API auth → 4. Validate manifest or reconcile live metadata → 5. Execute sequential metadata phases → 6. Add data sources → 6b. Publish fallback customizations → 6c. Verify tables → 6d. Write manifest → 7. Inspect generated files → 8. Type-check → 8.5. Offline profile reconciliation → 9. Summary\n\n---\n\n### Step 1 — Verify project & auth\n\nConfirm Power Apps mobile app:\n\nFor `--plan-only` or a planning-phase handoff, check the app files read-only and\nuse the shared proposal-only environment-context rule. Use the same non-persisting\nlookup before approval in a normal invocation. Incomplete or conflicting context\nreturns `NEEDS_CONTEXT` after read-only recovery; never remove the safety flags\nor redirect output into configuration to make planning succeed.\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nif [ ! -f power.config.json ] || [ ! -f app.config.js ]; then\n  echo \"BLOCKED: working_dir is not an initialized app\" >&2\n  exit 1\nfi\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fresolve-environment.js\" \"\u003Cselected-environment-id>\" --no-cache --require-tenant\n```\n\nRead `\u003Cselected-environment-id>` from this app's `power.config.json`. Capture the\n**environment URL** (for example `https:\u002F\u002Fcontoso.crm.dynamics.com`),\n**environment ID**, and **tenant ID** from the resolver for Step 3. Use the\nowner's absolute `working_dir` for every command and artifact.\n\n### Step 2 — Resolve plan\n\n**Telemetry checkpoint: `resolve_dataverse_schema_plan`**\n\nLook for `native-app-plan.md` in the project root:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\ntest -f native-app-plan.md\n```\n\n**Resolve the current request before consuming an existing plan.** A direct\nstandalone invocation must compare its requested tables\u002Fcolumns\u002Fservice\nchanges with the existing plan and read-only live evidence. Present and approve\nthat exact delta at Step 2.7 after shared planning validation, then save only\nthe accepted plan changes before implementation.\nIf the request adds nothing, verify the existing outcome and report a no-op;\ndo not apply other pending rows. If intent or scope is missing, ask or return\n`NEEDS_CONTEXT` without mutation. An explicit request to apply the whole existing\nplan still requires approval of the reconciled operation set.\nFor an approved child call, use only the supplied current `approved_scope`.\nIn either case, `--plan-only` returns the proposal and STOPs before plan saving,\napproval-receipt creation, service generation, or Steps 3–9; a planning-phase\ncaller has the same proposal-only boundary.\n\nBefore reading plan content, inspect `$ARGUMENTS` for the five fast-path\nartifact flags in Step 2a. When all are present, only confirm\n`native-app-plan.md` exists for hash validation; do not parse its Data Model\nsection or build operations\u002Fservice lists from Markdown.\n\nFor all non-fast-path Dataverse proposals from `\u002Fsetup-datamodel`, `\u002Fedit-app`, or\na standalone request, including an existing-plan delta, read and execute\n[dataverse-change-planning.md](..\u002F..\u002Fshared\u002Freferences\u002Fdataverse-change-planning.md).\nUse its scoped evidence\u002Fcontract for the diagram, architect, or inline path.\nAn approved child with `planning_snapshot`, `architect_evidence`, and\n`schema_contract` instead enters Step 2b to verify that supplied scope; it must\nnot rediscover or re-approve it. New-binding requests use this planning path;\nservice-only refreshes and retirements have already returned from their branches.\n\nOlder callers may not supply the new structured handoff. Do not assume compact\nevidence or complete approval from the caller's name. Without a complete approved\nhandoff, resolve the current requested delta and use this leaf's normal proposal\nand Step 2.7 approval path before execution. Resolve required invocation context\nfirst; a missing or conflicting child root still returns `NEEDS_CONTEXT` before\nproject access. Never silently replay the saved plan or treat `--skip-planning`\nas consent.\n\nAn approved legacy child without compact-planning artifacts keeps the supported\nMarkdown\u002Flive-reconciliation path below only when its current request, absolute\nroot, implementation phase, and exact approved delta are all established.\nNever use this legacy path to bypass a partial or invalid compact-planning\nhandoff, or to skip setup\u002Fstandalone proposal validation.\n\n**Legacy input only, if present and `\u003Coperation_manifest_mode> = fallback`\nwithout a shared scoped contract:** read the\n`## Data Model` section. Extract:\n- The target reconciliation table (`reuse` \u002F `extend` \u002F `create` \u002F `adapt` \u002F `defer` decisions and evidence)\n- The Mermaid ER diagram (informational)\n- The \"Creation Order\" tier list\n- Every table referenced by `## Screens`, identity resolution, related-entity fields, forms, dashboards, or shared hooks, including standard reused tables such as `systemuser`, `contact`, and `account`\n\nFor legacy callers without scoped planning artifacts, build\n`SERVICE_REQUIRED_TABLES` as the union of:\n1. every non-deferred row in Target Reconciliation (`reuse`, `extend`, `create`, or `adapt`);\n2. every table in Creation Order;\n3. every table named by screen\u002Fhook data requirements.\n\nKeep this full service inventory as retained context. Only the current approved\ndelta and its necessary dependencies enter live schema reconciliation. With a\ncomplete approved legacy handoff, proceed to Step 3 after this initialization;\ndo not repeat proposal Steps 2.5–2.7 or require new compact artifacts from that\nowner. A planning-phase caller still returns without implementation.\n\nFor the shared scoped path, use the normalized contract's non-deferred\n`serviceRequired` declarations instead. Keep the broader existing-app service\ninventory as retained context, not a registration or mutation work list.\n\n**Hard rule:** `reuse` means \"do not mutate schema\"; it does **not** mean \"skip generated service.\" If app code reads or writes a reused table, that table must be in `SERVICE_REQUIRED_TABLES`.\n\nCarry forward any `adapt` (auto-renamed) and `defer` (out-of-scope this run) decisions with their recorded reasons, and apply the alias map to every name you use. A data-modelling conflict never halts this skill — it resolves to `adapt` or `defer` and is reported in Step 9.\nThis classification belongs to planning: after approval, a changed decision\nreturns to the owner for revision\u002Fapproval before any write.\n\nFor a scoped handoff, restrict schema writes to the exact `approved_scope` delta;\nunaffected plan rows are context, not permission to replay their mutations.\nApply the same restriction to the newly approved standalone request delta.\nRetain the full required-service set for existing screens. If reconciliation\nwould change an approved name, storage target, or screen contract, return the\nproposed adaptation to the owner (or standalone user) before writing. Report\ndependent consumer changes for separate approval; do not silently rename\nunderneath the app or treat data approval as permission to edit screens.\n\n**If absent:** check `$ARGUMENTS` for diagram hints (`*.png`, `*.jpg`, `*.jpeg` filename, `erDiagram` keyword, `||--o{` cardinality syntax). \n\n- **Diagram hint present** → Path A (Step 2.5).\n- **No hint AND `$ARGUMENTS` describes what the app does** (the typical case) → silently take Path B (Step 2.6 — spawn architect). No prompt.\n- **No hint AND `$ARGUMENTS` is empty \u002F non-descriptive** → only then prompt with `AskUserQuestion`:\n\n  > \"How would you like to define the data model?\n  > (a) I have an existing ER diagram to upload (PNG\u002FJPG path, Mermaid syntax, or text description)\n  > (b) Let the data-model-architect agent analyze and propose one (default)\n  > (c) Cancel — I'll plan it elsewhere first\"\n\n  Recommend (b), but wait for an explicit answer. Empty input is not approval;\n  cancellation stops the workflow without planning or mutation. A supplied\n  description may select the read-only architect path, not approve its result.\n\nWith an existing plan and a new request, take the same diagram or architect\u002F\ninline proposal path for only that delta, then Step 2.7. Do not skip validation\nbecause a saved plan exists.\n\n#### Step 2a — Approved operation-manifest fast path\n\nWhen `$ARGUMENTS` supplies all five paths below, record\n`\u003Coperation_manifest_mode> = candidate`:\n\n- `--schema-contract \u003Cworking_dir>\u002F.tmp\u002Fdataverse-schema-contract.json`\n- `--approval-receipt \u003Cworking_dir>\u002F.tmp\u002Fmobile-plan-status.json`\n- `--execution-reconciliation \u003Cworking_dir>\u002F.tmp\u002Fdataverse-execution-reconciliation.json`\n- `--operation-manifest \u003Cworking_dir>\u002F.tmp\u002Fdataverse-operation-manifest.json`\n- `--publish-checkpoint \u003Cworking_dir>\u002F.tmp\u002Fdataverse-publish-pending.json`\n\nDo not reconstruct tables, columns, relationships, keys, payloads, tiers, or\nservice requirements from Markdown on this path. The gate-owned approval receipt binds\nthe exact structured contract content\u002Fhash, final plan hash, and final\nscreen\u002Fservice dependency list; `native-app-plan.md` remains the human review\nartifact.\n\nAn entirely absent fast-path handoff means\n`\u003Coperation_manifest_mode> = fallback` and preserves the standalone workflow\nbelow, beginning with Step 2 initialization. A partially supplied handoff, or\na supplied manifest\u002Fcontract\u002Freconciliation\u002Fcheckpoint that is malformed, stale,\nincomplete, or bound to different context\u002Ffiles, must fail closed: print the\nexact validation errors and return control to the orchestrator. Never jump to\nStep 4 without Step 2 initialization, partially trust a candidate, or mix its\noperations with agent-derived operations.\n\nFor an approved implementation candidate, continue to Step 3 and the existing\nStep 3c manifest validation; skip Steps 2b and 2.5–2.7. Supplied execution\nartifacts never override `--plan-only` or a planning-phase caller: those calls\nreturn the proposal without entering Step 3.\n\n#### Step 2b — Approved scoped planning context\n\nEnter only for a shared compact-planning handoff, not an approved legacy child\nor a creation fast-path candidate. The data owner passes the\nabsolute `planning_snapshot`, `architect_evidence`, and `schema_contract` paths\nplus accepted operations, `contract_sha256`, and `plan_sha256` in\n`approved_scope`. Require all fields and real implementation approval;\npartial, missing, changed, or mismatched context returns `NEEDS_CONTEXT` to\nthe owner, never a fallback to whole-plan replay or fresh approval inference.\n\nBefore Step 3, compare SHA-256 of the contract and final plan file bytes with\nthose frozen approval hashes, verify the selected target identity, and run the\nshared compact-evidence and decision validators against the supplied artifacts.\nDo not overwrite either hash to accept changed files. Read mutation intent\nfrom the normalized scoped contract, not historical Markdown rows; retain\nunaffected generated services. A proposal-only caller returns without mutation.\n\nAfter success, keep `\u003Coperation_manifest_mode> = fallback` for the existing\nstandalone execution\u002Freconciliation path, skip Steps 2.5–2.7, and proceed to\nStep 3. Planning evidence is not fresh write evidence: Step 4 still reconciles\nonly this accepted delta and its required dependencies against the live target.\nDo not fabricate a `create-mobile-app` receipt or partially supply Step 2a flags.\n\n### Step 2.5 — Path A: Parse user-provided diagram\n\nUsed when the user has an existing diagram from another tool (Visio, dbdiagram.io, screenshot, hand-drawn).\n\nAccept three input formats:\n\n| Format | How |\n|---|---|\n| **Image path** (`*.png` \u002F `*.jpg` \u002F `*.jpeg`) | Use `Read` on the file path. The vision-capable model extracts entities, columns, relationships. |\n| **Mermaid syntax** | User pastes a `erDiagram` block in chat. Parse the entities, columns, and `\\|\\|--o{` cardinalities directly. |\n| **Text description** | Parse the requested entities\u002Frelationships as intent, then use the shared snapshot-only architect handoff with the text and scoped compact evidence. |\n\nWhichever format, normalize into the same structure used by the planner agent:\n\n```yaml\npublisherPrefix: \u003Cfrom detected publisher prefix or user>\ntables:\n  - logicalName: contoso_servicevisit\n    displayName: Service Visit\n    status: new   # new | extend | reuse\n    columns: [...]\n    relationships: [...]\n```\n\nThen follow the shared planning workflow to generate `_dm_section.md` with\nthe Mermaid diagram and a normalized `.tmp\u002Fdataverse-schema-contract.json`\nfrom compact evidence. Run its decision validation for the diagram just as for\nan architect result, then continue to Step 2.7. Do not save the live plan here.\n\n### Step 2.6 — Path B: Spawn architect agent\n\nIf the user picked Path B, use the shared workflow's\n`mobile-app:data-model-architect` handoff with `Dataverse planning mode: required`,\nthe absolute snapshot\u002Fcompact-evidence paths, the scoped request, and planning\u002F\nproposal-only context. Require `_dm_section.md` and the normalized contract.\nIf the host cannot spawn agents, produce both inline from that same evidence.\nUse the shared structured-signal recovery before generic retries. A diagram\nparse failure requires resolving the intended entities, not silently inventing\na replacement model.\n\nIf they need new tables and refuse both paths, recommend `\u002Fsetup-datamodel` for\ndata-only planning and stop. Do not start another workflow without a new request.\n\n### Step 2.7 — Validate and approve the standalone delta\n\nFor every new standalone proposal, require exit `0` from\n`validate-dataverse-planning-decisions.js` for the current normalized contract\nand snapshot before presenting the existing approval gate. This includes\ndiagrams, architect results, inline fallbacks, and edits to an existing plan.\nIf either `--plan-only` or a planning-phase caller applies, return the validated\nproposal and STOP before saving the live plan or granting execution permission.\n\nOtherwise show the exact delta and its concerns with `EnterPlanMode`. Approval\nsaves only that delta into `native-app-plan.md`, preserving unaffected sections.\nFreeze the shared planning paths, accepted operations, and current\n`contract_sha256` \u002F `plan_sha256` as this invocation's approved context; verify\nit using Step 2b before implementation. Revision repeats planning validation;\ncancellation stops without saving or mutation. Already-approved scoped child\ncalls and valid creation fast-path calls do not repeat this gate.\n\n### Step 3 — Setup Dataverse Web API auth\n\nRequired only after implementation approval. For approved existing-table\nbindings with no schema mutation, skip to Step 6, not the schema-write phases.\n\n#### Step 3a — Environment consistency check\n\nThe Power Apps CLI (`$PA`) and `az` authenticate independently — they can point to different accounts. Verify `power.config.json` resolves and `az` can token for the target tenant before making any Dataverse API calls:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nENVIRONMENT_ID=$(node -p \"require('.\u002Fpower.config.json').environmentId\") || exit 1\nENV_JSON=$(node \"${PLUGIN_ROOT}\u002Fscripts\u002Fresolve-environment.js\" \"$ENVIRONMENT_ID\") || exit 1\necho \"$ENV_JSON\"\naz account show --query \"{user: user.name, tenant: tenantId}\" -o json\n```\n\nCompare the resolved environment URL with `\u003CenvUrl>` captured in Step 1. If they differ, **STOP** and warn:\n\n> \"⚠️ Environment mismatch detected:\n> - resolver reports: `\u003Cresolved_env_url>`\n> - This project targets: `\u003CenvUrl>`\n>\n> The Dataverse API token comes from `az`, which must target the same tenant as the selected environment. Run:\n> ```bash\n> cd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\n> az login --tenant \u003Ctenant-id>      # switch az to the right tenant\n> ```\n> Then re-run `\u002Fadd-dataverse`.\"\n\n**Do NOT proceed with table creation if environments don't match** — you'll create tables in the wrong org.\n\n#### Step 3b — Acquire token\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\naz account show --query \"user.name\" -o tsv\n```\n\nIf empty, instruct `az login` and stop.\n\n**Script invocation contract — read this once, all subsequent calls in this skill follow it:**\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> \u003CMETHOD> \u003CapiPath> \\\n  [--body '\u003Cjson>'] [--include-headers] \\\n  --tenant-id '\u003CtenantId-from-resolve-environment>'\n```\n\n- Three positional args, in order: `\u003CenvUrl>`, `\u003CMETHOD>` (GET \u002F POST \u002F PATCH \u002F DELETE), `\u003CapiPath>` (everything after `\u002Fapi\u002Fdata\u002Fv9.2\u002F`).\n- **Body is a flag, not positional.** `--body '\u003Cjson>'` — required for POST\u002FPATCH, never for GET\u002FDELETE. Forgetting `--body` and passing the JSON as a 4th positional arg returns a usage error.\n- `--include-headers` adds response headers (needed for `OData-EntityId` after a record create).\n- Output is JSON: `{ \"status\": \u003Ccode>, \"data\": \u003Cbody> }`. Token refresh on 401 and back-off on 429 are automatic — never wrap with manual retry.\n\n**Pass the resolved tenant explicitly (HARD — saves discovery and survives fresh shells).** `resolve-environment.js` already returned `tenantId` in Step 1. Substitute that literal value into every `--tenant-id` argument; do not rely on an exported or shell-local variable because separate tool executions may use fresh shells.\n\nIf the tenant is unknown, omit `--tenant-id` — discovery still works, it is just slower.\n\nAcquire a Dataverse access token and verify connectivity:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> GET WhoAmI \\\n  --tenant-id '\u003CtenantId-from-resolve-environment>'\n```\n\nThe explicit tenant is also reused for token refresh after a 401 and takes\npriority over shell environment variables and Azure account discovery.\n\n`WhoAmI` is the Dataverse identity endpoint — capital W\u002FA\u002FI (case-sensitive). The response gives `UserId`, `BusinessUnitId`, `OrganizationId` but **does NOT include the publisher prefix**. To get the publisher prefix, query the solution's publisher (defaults to `Default`; pass a different solution name if the env uses a custom solution):\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fdetect-publisher-prefix.js\" \u003CenvUrl> [solutionName] \\\n  --tenant-id '\u003CtenantId-from-resolve-environment>'\n# solutionName defaults to \"Default\" if omitted\n```\n\nThis runs the OData query:\n`\u002Fapi\u002Fdata\u002Fv9.2\u002Fsolutions?$select=uniquename&$expand=publisherid($select=customizationprefix)&$filter=uniquename eq '\u003CsolutionName>'`\n\nCapture `customizationprefix` from the solution's publisher (typical value: `cr123` → schema names like `cr123_jobsite`). Also capture the solution `uniquename` — needed for the `--solution` flag on every Step 5 \u002F 5b POST so artifacts land in our solution rather than landing wherever Dataverse defaults. Write both to `memory-bank.md` Power Platform context block.\n\nRequires the user to hold **System Administrator** or **System Customizer** in this environment.\n\nWhen `\u003Coperation_manifest_mode> = candidate`, validate the manifest now against\nthe resolved environment, tenant (when available), publisher, solution, current\nplan bytes, structured-schema bytes, and fresh reconciliation bytes:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fbuild-dataverse-operation-manifest.js\" \\\n  --validate '\u003Coperation-manifest-path>' \\\n  --contract '\u003Cschema-contract-path>' \\\n  --approval-receipt '\u003Capproval-receipt-path>' \\\n  --reconciliation '\u003Cexecution-reconciliation-path>' \\\n  --plan '\u003Cworking_dir>\u002Fnative-app-plan.md' \\\n  --environment-id \"\u003CenvironmentId>\" \\\n  --env-url \"\u003CenvUrl>\" \\\n  --tenant-id \"\u003CtenantId>\" \\\n  --publisher-prefix \"\u003Ccustomizationprefix>\" \\\n  --solution \"\u003Csolution-uniquename>\" \\\n  --publish-checkpoint '\u003Cpublish-checkpoint-path>' \\\n  --require-executable\n```\n\nValidation deterministically rebuilds the expected manifest from the bound\nstructured schema, fresh reconciliation, plan, context, and pending-publish checkpoint, then\ncompares the complete decisions, services, aliases, phases, API paths, and\nbodies. If validation fails, print every reported mismatch and fail closed to\nthe orchestrator. Do not execute or salvage individual operations and do not\nswitch a supplied candidate to the standalone fallback.\n\nValidate with `--require-executable`. Step 8 already performed the one fresh\nbounded reconciliation for every approved exact table and all of its\ncolumns\u002Frelationships\u002Fkeys, including the child\u002Fparent\u002FM:N relationship\ncapability managed properties. Missing capability evidence fails closed. If\nthe manifest remains non-executable, report its\nverification conflicts to the orchestrator. Do not add another read loop,\nchange an approved decision, or enter fallback mode. A non-executable\ncandidate authorizes no metadata write.\n\nIf validation with `--require-executable` succeeds, set\n`\u003Coperation_manifest_mode> = valid` and continue directly to Step 5's manifest\nexecution branch. This is the fast-v2 path: it skips the repeated\nagent-driven full reconciliation, not any safety check.\n\n### Step 4 — Reconcile every planned table and column against the target\n\n**Telemetry checkpoint: `reconcile_dataverse_schema`**\n\nIf `\u003Coperation_manifest_mode> = valid`, print:\n\n> `✓ Approved operation manifest validated — complete fresh reconciliation and derived metadata coverage are bound to this environment.`\n\nUse its `decisions` as the reconciliation matrix and skip the remainder of\nStep 4\u002F4a. Continue to Step 5. A valid manifest has no `unverified` items; its\nexplicit `reuse`, `adapt`, and `defer` rows remain visible in the final\nsummary.\n\n**Print before starting:**\n> \"→ Reconciling every planned table and column against live target metadata before any write…\"\n\nDo not use the custom-table list as the source of truth, and do not issue one request per table. Fetch **every** plan entry (`Reuse`, `Extend`, or `Create`) — including standard and managed dependencies — in a **single** filtered query that also expands their columns:\n\nOn the scoped planning path, \"every plan entry\" means only the approved\nnormalized contract and its required dependencies, never all saved-plan rows.\nDo not substitute the planning snapshot or inventory cache for this live read.\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> GET \\\n  \"EntityDefinitions?\\$select=MetadataId,LogicalName,SchemaName,IsCustomEntity,IsManaged,IsCustomizable,CanCreateAttributes,PrimaryIdAttribute,PrimaryNameAttribute&\\$filter=LogicalName eq '\u003Ctable1>' or LogicalName eq '\u003Ctable2>'&\\$expand=Attributes(\\$select=MetadataId,LogicalName,AttributeType,AttributeTypeName,RequiredLevel,IsManaged,IsCustomizable,IsPrimaryId,IsPrimaryName,SourceType,SourceTypeMask)\" \\\n  --tenant-id '\u003CtenantId-from-resolve-environment>'\n```\n\nBuild the `$filter` by OR-ing every planned logical name. This is the [documented way to query multiple table definitions at once](https:\u002F\u002Flearn.microsoft.com\u002Fpower-apps\u002Fdeveloper\u002Fdata-platform\u002Fquery-schema-definitions#basic-retrievemetadatachanges-example), and it replaces 2N requests (one entity GET plus one attributes GET per table) with one. Keep the `$expand` `$select` list to base `AttributeMetadata` properties only — a single query [cannot cast to a derived column type](https:\u002F\u002Flearn.microsoft.com\u002Fpower-apps\u002Fdeveloper\u002Fdata-platform\u002Fquery-schema-definitions#evaluate-other-options-to-retrieve-schema-definitions), so fetch `OptionSet` details separately for the rare column that needs them.\n\nRead the results as follows:\n\n- **A planned name present in `value[]`** — the table exists. Cache its expanded `Attributes` as that table's **attribute snapshot** for Steps 5a and 5b.\n- **A planned name absent from `value[]`** — the table does not exist. This is the equivalent of a 404 in the matrix below.\n- Interpret `IsCustomizable` and `CanCreateAttributes` as managed properties and read their `.Value` fields.\n\nIf the batched query itself fails (non-2xx), retry it once; if it fails again, split it into per-table queries so one unreadable name cannot hide the rest. Any name still unreadable after that is `unverified`: STOP before writes for that reconciliation scope. Authentication, permission, timeout, and malformed-response failures are not evidence that a name is free. If the URL would exceed a practical length with very many tables, split it into a few filtered queries — still far fewer than one request per table.\n\n**Only if the plan contains alternate keys or M:N relationships**, add the matching expands so Steps 5b and 5d never need their own per-item probes. `EntityDefinitions` also supports expanding [`Keys`, `ManyToManyRelationships`, `ManyToOneRelationships`, and `OneToManyRelationships`](https:\u002F\u002Flearn.microsoft.com\u002Fpower-apps\u002Fdeveloper\u002Fdata-platform\u002Fquery-schema-definitions#evaluate-other-options-to-retrieve-schema-definitions):\n\n```text\n&$expand=Attributes($select=...),Keys($select=SchemaName,KeyAttributes,EntityKeyIndexStatus),ManyToManyRelationships($select=SchemaName)\n```\n\nDo not add these expands when the plan has no keys or M:N relationships — they enlarge the response for no benefit, and standard tables carry many of both.\n\n#### Step 4a — Targeted derived-metadata barrier\n\nThe base attribute snapshot is sufficient for ordinary columns, but it cannot\nprove that a same-named lookup, choice, Boolean, or computed column has the same\nsemantics. Before classifying any such existing column as compatible:\n\n1. Write the planned derived-column contract to\n   `\u003Cworking_dir>\u002F.tmp\u002Fderived-metadata-expected.json`. Each row contains:\n   `table`, `logicalName`, `kind`, `type`, `sourceType`, plus:\n   - `lookupTarget` for lookups;\n   - exact integer\u002Flabel `options` for Choice, MultiSelect Choice, and Boolean;\n   - exact `sourceTypeMask` and serialized `formulaDefinition` for an explicitly\n     approved, maker-created computed dependency.\n2. Build one `BATCH-METADATA` GET operation list for the affected existing\n   tables only. Reuse one process\u002Ftoken and query:\n   - `ManyToOneRelationships` once per child table containing planned lookups;\n   - the applicable derived attribute collections\n     (`PicklistAttributeMetadata`, `MultiSelectPicklistAttributeMetadata`,\n     `BooleanAttributeMetadata`) once per table\u002Ftype, expanding `OptionSet`;\n   - the applicable typed attribute collection once per table\u002Ftype for any\n     explicitly reused computed column, selecting\n     `LogicalName,SourceType,SourceTypeMask,FormulaDefinition`.\n\n   Do not issue one process per column and do not scan every customizable table.\n   The exact planned names from Step 4 are the scope.\n   Write the operation array to\n   `\u003Cworking_dir>\u002F.tmp\u002Fderived-metadata-operations.json`, then run:\n\n   ```bash\n   cd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\n   node \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> \\\n     BATCH-METADATA derived-reconciliation \\\n     --operations \"$(cat '\u003Cworking_dir>\u002F.tmp\u002Fderived-metadata-operations.json')\" \\\n     --tenant-id '\u003CtenantId-from-resolve-environment>'\n   ```\n\n   Do not pass `--continue-on-error`; the first unreadable required metadata\n   collection must stop the barrier.\n3. Any non-2xx response, missing result slot, malformed option metadata, absent\n   lookup target, or unavailable `FormulaDefinition` makes that scope\n   `unverified`. **STOP before writes.** Authentication, throttling, permission,\n   and parse failures are never compatibility evidence.\n4. Normalize the live results into\n   `\u003Cworking_dir>\u002F.tmp\u002Fderived-metadata-live.json`. Each row uses:\n   `table`, `logicalName`, `type`, `sourceType`, `sourceTypeMask`,\n   `lookupTargets`, `options: [{ value, label }]`, and `formulaDefinition`.\n   Lookup target arrays must contain exactly the approved target. Choice\n   mappings must be non-empty with unique integer values and non-empty labels;\n   Boolean mappings must contain exactly values 0 and 1. Then run:\n\n   ```bash\n   cd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\n   node \"${PLUGIN_ROOT}\u002Fscripts\u002Fvalidate-derived-metadata.js\" \\\n     --expected '\u003Cworking_dir>\u002F.tmp\u002Fderived-metadata-expected.json' \\\n     --actual '\u003Cworking_dir>\u002F.tmp\u002Fderived-metadata-live.json'\n   ```\n\n5. A lookup is compatible only when its complete target set matches. Planned choice\n   values must exist with the same labels; extra live values are allowed.\n   Ordinary planned columns require `SourceType` 0. A maker-created computed\n   dependency is reusable only when its source type, source-type mask, and exact\n   `FormulaDefinition` match the approved artifact; the `Invalid` mask bit\n   always blocks reuse.\n\nThis phase is read-only and uses the V2 long-lived executor. It must not add\nper-column child-process\u002Ftoken overhead back into the fast path.\n\nBuild and print a reconciliation matrix before Step 5:\n\n| Target result | Table decision | Column decisions | Action |\n|---|---|---|---|\n| Present; all planned base and derived metadata compatible | `reuse` | existing columns `reuse` | No schema write. |\n| Present; custom columns missing; table customizable and can create attributes | `extend` | compatible `reuse`; absent custom `create` | Queue missing ordinary columns for sequential creation; relationships remain Pass 2. |\n| Absent; plan says Create; logical name uses the verified publisher prefix | `create` | ordinary columns `create` inline; lookups deferred | Create once after the complete-payload self-check. |\n| Absent; plan says Reuse\u002FExtend or dependency is standard\u002Fmanaged\u002Frequired-existing | `defer` | dependent columns `defer` | Never recreate a standard or managed table. Drop the dependent lookups\u002Fcolumns from this run, continue with everything else, and list them under Deferred in Step 9. |\n| Present; same-name column has incompatible `AttributeType` \u002F `AttributeTypeName.Value` | `extend` | incompatible column `adapt` | Auto-rename the planned column via the probe sequence below, record it in the alias map, and create it alongside the existing one. Never modify or delete the existing column. |\n| Present; columns missing but `IsCustomizable.Value=false` or `CanCreateAttributes.Value=false` | `reuse` | missing columns `defer` | The target cannot be extended by this workflow. Reuse the columns that do exist, drop the rest from this run, and list them under Deferred in Step 9. |\n| Batched query failed (non-2xx) after retry and per-table split | `unverified` | unknown | STOP before writes for the affected reconciliation scope and surface the concrete environment\u002Fauth\u002Fpermission error. |\n\n`replace` is not an automatic state in this workflow. Replacing a table or column requires an explicitly approved migration with dependency analysis and data movement, so a conflict resolves to `adapt` (rename beside it) or `defer` (leave it out) instead — both of which leave existing data untouched.\n\n**Decide-before-write barrier (HARD):** finish reconciliation for every table and column before the first metadata write. Every item must come out of Step 4 as `reuse`, `extend`, `create`, `adapt`, or `defer` — never as an unresolved conflict. Deciding renames up front is what keeps relationships, screens, and sample data pointing at the same names.\n\n**No dead ends (HARD):** a data-modelling conflict must never stop the run. Adapt it (rename beside the existing object) or defer it (drop it from this run), then keep going and report it in Step 9. Only environment faults stop this skill — failed auth, an environment mismatch, or a target the user has no privilege to write to. Those are not data-modelling problems and the user cannot resolve them by editing the plan.\n\n**Idempotency criterion (HARD):** re-running this skill against an already-applied plan MUST perform **zero** metadata writes. Every table, column, relationship, key, and calc column resolves to `reuse` or an \"already exists, skipped\" outcome from the Step 4 snapshot. If a re-run issues any POST, the reconciliation missed something — report it rather than writing. Use this as the acceptance check after any change to Steps 4, 5, or 5a–5d.\n\n### Step 5 — Create \u002F extend tables\n\n**Telemetry checkpoint: `apply_dataverse_schema_changes`**\n\n#### Valid operation-manifest execution branch\n\nWhen `\u003Coperation_manifest_mode> = valid`, do not have an agent rebuild request\nbodies. Read `execution.phases` in this fixed order:\n\n1. `tableCreates` — dependency-tier table creates with all ordinary columns\n   inline;\n2. `extensions` — missing ordinary columns on existing tables;\n3. `relationships` — lookups\u002F1:N and M:N after both endpoints exist;\n4. `alternateKeys` — after target tables and columns exist;\n5. `publish` — one `PublishXml` operation only when earlier phases contain\n   writes or a bound publish-pending checkpoint requires retry.\n\nFor each non-empty phase, write just that phase's `operations` array to\n`\u003Cworking_dir>\u002F.tmp\u002Fdataverse-operation-phase-\u003Cname>.json`. Read\n`integritySha256` and `binding.reconciliationSha256` from the validated\nmanifest, then execute every phase with the same project-local atomic journal:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nEXECUTION_JOURNAL='\u003Cworking_dir>\u002F.tmp\u002Fdataverse-metadata-execution-journal.json'\nALL_MANIFEST_OPERATIONS='\u003Cworking_dir>\u002F.tmp\u002Fdataverse-operation-all.json'\n# Write the flattened operations from every manifest phase to ALL_MANIFEST_OPERATIONS once.\n\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> \\\n  BATCH-METADATA \"manifest-\u003Cphase-name>\" \\\n  --operations \"$(cat '\u003Cworking_dir>\u002F.tmp\u002Fdataverse-operation-phase-\u003Cname>.json')\" \\\n  --solution \"\u003Csolution-uniquename>\" \\\n  --tenant-id \"\u003CtenantId>\" \\\n  --journal \"$EXECUTION_JOURNAL\" \\\n  --manifest-file \"$OPERATION_MANIFEST\" \\\n  --manifest-hash \"\u003Cmanifest.integritySha256>\" \\\n  --reconciliation-hash \"\u003Cmanifest.binding.reconciliationSha256>\" \\\n  --manifest-operations \"$(cat \"$ALL_MANIFEST_OPERATIONS\")\"\n```\n\nWait for the entire phase to finish before starting the next. This executor is\nnot OData `$batch`; it sends one request at a time, reuses one token, preserves\nmanifest order, and stops on the first non-2xx response. Never pass\n`--continue-on-error`, never parallelize phases or operations, and never\nexecute `service.requiredTables` through BATCH-METADATA.\nThe runner verifies the manifest file's integrity hash, requires the supplied\noperation array to equal one complete manifest phase byte-for-byte, and rejects\nthat phase until every operation in preceding phases is journal-complete.\n\nThe runner atomically writes `inFlight` before each request and records each\nsuccessful operation before moving to the next. Completed fingerprints are\nbased on stable request identity, not the positional index; the complete\nmanifest's unique zero-based order is validated separately. Completed\nfingerprints are skipped on an exact resume. If a process may have exited after Dataverse\naccepted a request but before the journal completion write, the runner fails\nwith `UNCERTAIN_METADATA_OPERATION`; do not replay the old operation array.\nTransport loss during a metadata mutation is immediately uncertain and is\nnever retried in-process; reads may retain transport retry behavior.\nPerform a new bounded exact reconciliation, rebuild and revalidate the full\nmanifest, then resume with both new hashes. The runner mechanically treats an\nuncertain operation omitted by the new manifest as already applied\u002Fsuperseded,\nor retries it only when the fresh reconciliation proves it is still required.\n\nIf `summary.metadataOperationCount` is zero, issue **zero metadata POSTs** and\nprint `↻ Dataverse schema already fully applied — zero metadata writes.` This\nis the required idempotent rerun behavior after successful publish. A manifest\nwith zero schema operations but one checkpoint-driven `PublishXml` operation\nmust run that publish retry; it is not a zero-write completion.\n\nThe manifest builder writes\n`\u003Cworking_dir>\u002F.tmp\u002Fdataverse-publish-pending.json` before any schema POST when\npublish will be required. Leave this checkpoint in place after any schema or\npublish failure. Delete it only after the validated `publish` phase returns\nsuccess. The next build validates its environment\u002Fsolution\u002Fplan\u002Fcontract\nbinding and integrity, merges its table list into the publish phase, and\ntherefore retries `PublishXml` even when every schema create is now\nidempotently skipped.\n\nOn a hidden POST-time name collision, stop at the failed operation and discard\nall not-yet-run phase arrays. The execution script must not choose `Adapt` or a\nrename. Return to the existing planning revision path so the structured schema\nartifact carries the approved `Adapt` names. Then perform a fresh bounded\nreconciliation and regenerate the complete aliases, downstream relationship\nand key bodies, service-required names, phases, manifest hashes, and phase\nfiles. Revalidate before resuming through the journal. Never continue an old\narray after a rename. Any non-collision failure stops the metadata path with\nits exact result.\n\nBefore overwriting the old manifest, preserve its path. After the revised plan\nand structured schema are approved through the existing flow, the top-level\nplanner\u002Forchestrator must refresh the structured service dependencies and\n`mobile-plan-status.json` receipt. This skill cannot create or restamp it.\nBind the contract through that pre-existing receipt, then roll the existing\npublish checkpoint forward:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fbuild-dataverse-operation-manifest.js\" \\\n  --roll-forward-checkpoint \"$PUBLISH_CHECKPOINT\" \\\n  --previous-manifest \"$OPERATION_MANIFEST\" \\\n  --journal \"$EXECUTION_JOURNAL\" \\\n  --contract \"$SCHEMA_CONTRACT\" \\\n  --approval-receipt \"$APPROVAL_RECEIPT\" \\\n  --plan '\u003Cworking_dir>\u002Fnative-app-plan.md' \\\n  --output \"$PUBLISH_CHECKPOINT\" \\\n  --environment-id \"\u003CenvironmentId>\" \\\n  --env-url \"\u003CenvUrl>\" \\\n  --tenant-id \"\u003CtenantId>\" \\\n  --publisher-prefix \"\u003Ccustomizationprefix>\" \\\n  --solution \"\u003Csolution-uniquename>\"\n```\n\nThis retains prior checkpoint bindings\u002Ftables as integrity-protected history,\nkeeps earlier successful tables publication-pending, and maps only the\njournal-proven failed collision table to its revised in-contract alias. It\nfails closed if a completed write would disappear from the revised contract or\nchange definition: completed tables\u002Finline columns, extension columns, image\nconfiguration PUTs, relationships (including cascade behavior), and alternate keys must each map\nto an equivalent revised structured component. Completed image PUTs retain their\nfull hash-bound definitions only when the revised image requirements remain\ncompatible; they stay publication-pending without replaying the update. It also\nrejects any unrelated out-of-contract publish target. Only after this succeeds\nmay Step 8 overwrite the operation manifest.\n\nThe manifest builder never emits calculated\u002Frollup\u002Fformula creation. Reused\ncomputed dependencies have already crossed the exact derived-metadata barrier;\nunsupported projections are explicit `defer` rows. After the `publish` phase succeeds, delete the publish checkpoint and invalidate the planning-only inventory cache:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode -e \"const checkpoint = process.argv[1]; if (checkpoint) require('node:fs').rmSync(checkpoint, { force: true });\" \\\n  \"${PUBLISH_CHECKPOINT:-}\"\nif ! node \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-inventory-cache.js\" \\\n  --file '\u003Cworking_dir>\u002F.tmp\u002Fdataverse-inventory-cache.json' --invalidate; then\n  printf 'NEEDS_RECOVERY: dataverse-inventory-cache\\n' >&2\n  exit 2\nfi\n```\n\n**Cache-invalidation recovery (both publish paths):** a nonzero exit returns\ncontrol to the foreground agent for local recovery, not success. Diagnose the\ncache path or filesystem permission error and allow at most two targeted retries\nof the invalidation command. Preserve the successful publish result and execution\njournal; do not replay metadata writes or publish to repair a local cache failure.\nDo not reuse the failed cache or continue to service generation, verification,\nsample data, or offline setup until invalidation exits `0`. If local recovery\ncannot complete, report the remaining blocker and resume this cleanup after it is\nresolved; never mark the phase complete just because publication succeeded.\n\nOnly after successful invalidation continue to Step 6. When there were zero writes and no checkpoint,\ncontinue without deleting anything. Skip the\nfallback mutation instructions in Steps 5a–5d and skip Step 6b because publish\nwas already part of the validated phase order.\n\n**Print before starting:**\n> \"→ Creating\u002Fextending \u003CN> tables in tier order (sequential — Dataverse serializes metadata writes). For each: pre-flight check, then 'Creating \u003Ctable>…' before the POST and '✓ \u003Ctable>' on 2xx response.\"\n\n> **⚠️ Concurrency rule — do not violate.** All Dataverse metadata operations in Steps 5, 6, and 6b are **strictly sequential**: issue one HTTP request, wait for a 2xx response, then issue the next. Do NOT parallelize or use OData `$batch`. Dataverse serializes metadata writes via an exclusive lock; parallel calls return `429 TooManyRequests`, `MetadataLockHeldException`, or `404 EntityNotFound` for lookups whose parent hasn't committed yet.\n>\n> Specifically:\n> - **Within a tier:** create tables one at a time.\n> - **Across tiers:** Tier 0 fully done (all tables + all columns committed) before any Tier 1 POST.\n> - **Lookups:** POST to `\u002FRelationshipDefinitions` only **after** both endpoint tables exist and have returned 2xx.\n> - **Extensions:** column POSTs to an existing table are also serial — same lock applies.\n\nFor multiple already-reconciled operations, prefer the local `BATCH-METADATA`\nexecutor. It is **not** OData `$batch`: one Node process reuses one token and\nissues requests strictly one at a time in array order, stopping on the first\nnon-2xx response by default.\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> \\\n  BATCH-METADATA schema-writes \\\n  --operations '\u003Cordered-json-array>' \\\n  --solution '\u003Csolution-uniquename-from-memory-bank>' \\\n  --tenant-id '\u003CtenantId-from-resolve-environment>'\n```\n\nEach operation is `{ \"index\", \"method\", \"apiPath\", \"body\" }`; an operation may\noverride the command-level `solution`. Build the array only after the full\nmetadata snapshot and desired\u002Flive diff. Preserve dependency order: new tables\nwith ordinary columns inline, extension columns, relationships, projections,\nthen alternate keys. Never pass `--continue-on-error` for schema creation. The\nresult includes per-operation `status` and `durationMs`; after a failure,\nreconcile that component and resume with only the remaining operations.\n\n#### Step 5a — Pre-flight collision check (from the Step 4 snapshot)\n\nBefore each create, confirm the target name is actually free: name-prefix collisions from stale solutions, reserved system names, and soft-deleted tombstones all fail the POST, and Dataverse takes ~1 minute to return the conflict error. A failure here can leave Tier 0 partially created and make a Tier 1 lookup fail on a phantom parent. Step 4 already collected this evidence for every planned name, so this step reads it rather than re-querying.\n\n**For every `Create` entry, resolve its target state from the Step 4 batch — do not re-query per table.** Step 4 already fetched every planned logical name, so reuse that result:\n\n| Step 4 result for this name | Meaning | Action |\n|---|---|---|\n| **Absent from `value[]`** | Name is free | Proceed with POST. |\n| **Present** + `IsCustomEntity: true` + `MetadataId` matches memory-bank | We created this earlier — idempotent re-run | Skip the POST, mark as created, continue. |\n| **Present** + `IsCustomEntity: true` + `MetadataId` *not* in memory-bank | Foreign collision | Reconcile live columns and customization properties below; never auto-extend an uncustomizable target. |\n| **Present** + `IsCustomEntity: false` | Reserved system table name | Auto-recover via rename (see below). |\n\nOnly re-probe a single name when Step 4's batch did not cover it (for example a rename candidate generated later in this step):\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> GET \\\n  \"EntityDefinitions(LogicalName='\u003Cprefix>_\u003Ctable>')?\\$select=MetadataId,LogicalName,IsCustomEntity,IsManaged,IsCustomizable,CanCreateAttributes\" \\\n  --tenant-id '\u003CtenantId-from-resolve-environment>'\n```\n\nTombstones and hidden collisions are **not** reliably visible to either form — Dataverse can report a name as free and still reject the POST minutes later. Those are caught by the POST-time collision rescue below, which is the real safety net:\n\n| POST response | Meaning | Action |\n|---|---|---|\n| **5xx** with `0x80060890` or message `\"object with same name exists in solution\"` | Tombstone (soft-deleted, ~30 min purge TTL) | Auto-recover via rename (see below). |\n| **400** with `0x80044363`, `\"schema name ... is not unique\"`, or `\"same name already exists\"` | Hidden Dataverse collision \u002F recent-delete tombstone | Auto-recover via rename (see below), then retry the POST once. |\n\n**Important:** treat a POST-time collision as a recoverable name conflict, not a data-model failure — the schema name can stay reserved internally after a delete even when metadata reports it as free.\n\n#### Auto-recovery — reuse\u002Fextend first, rename as last resort\n\n**Priority order when Step 5a hits a name collision:**\n\n1. **Adopt as Extend (preferred)** — only if the existing table is the same concept, every same-name column is type-compatible, planned missing columns are custom additions, and live `IsCustomizable.Value` plus `CanCreateAttributes.Value` both permit extension. Add only the missing columns via Step 5b and log `→ Extending existing \u003Coriginal> with \u003CN> missing columns.`\n2. **Adopt as Reuse** — if the existing table's schema already covers all planned columns: skip Step 5b for this entry, keep it in Step 6 for service generation. No prompt. Log `→ Reusing existing \u003Coriginal> (all required columns present).`\n3. **Rename and Create (last resort)** — only when the existing table is a fundamentally different entity (e.g., planned table is an inspection log but existing `\u003Coriginal>` is a payroll record — incompatible concept, incompatible columns). Prompt the user before proceeding.\n\n**When to auto-decide vs. prompt:**\n\n| Situation | Action |\n|---|---|\n| Foreign collision + compatible concept\u002Fschema + extension allowed | Auto-Extend (no prompt) |\n| Foreign collision + all planned columns present | Auto-Reuse (no prompt) |\n| Foreign collision + incompatible column or extension forbidden | Auto-rename the conflicting column beside it, or defer it (no prompt) |\n| Foreign collision + incompatible concept | Prompt (see below) |\n| Reserved system name | Auto-rename (no prompt) |\n| Tombstone (0x80060890 \u002F same-name-exists) | Auto-rename (no prompt) |\n\n**For the incompatible-concept case only** — prompt via `AskUserQuestion`:\n\n```\n| Option | What it means |\n|---|---|\n| Rename and Create (default) | Use a free custom logical name for the genuinely different entity. Existing table stays untouched. |\n| Reuse existing as-is | Point the generated services at the existing table and skip the planned columns it lacks. |\n```\n\nNever offer Extend for an incompatible concept or column shape. This prompt is a preference, not a gate: an empty, skipped, or unanswered response defaults to **Rename and Create** so the run always proceeds.\n\nMaintain a run-level logical-name alias map for every auto-rename. Example:\n\n```json\n{ \"cr3e9_aircraft\": \"cr3e9_aircraftv2\" }\n```\n\nBefore building any later table, column, lookup relationship, sample-data payload, service-reference text, or screen data spec, resolve logical names through this map. A rename that only changes the table POST but leaves relationships\u002Fscreens\u002Fsample data pointing at the old name is a bug.\n\n**Auto-rename probe sequence** (cap at 4 probes — only used for reserved\u002Ftombstone cases):\n\n```\n\u003Coriginal>v2  →  \u003Coriginal>v3  →  \u003Coriginal>2  →  \u003Coriginal>copy\n```\n\nFor each candidate in order, GET `EntityDefinitions(LogicalName='\u003Ccandidate>')?$select=MetadataId,IsCustomEntity`:\n- 404 → free, **take it**, stop probing.\n- 200 or 5xx (collision) → next candidate.\n\nIf all 4 collide, keep probing `\u003Coriginal>3`, `\u003Coriginal>4`, … through `\u003Coriginal>20`. This sequence is designed never to dead-end: if even those collide, use `\u003Coriginal>\u003C4-char run token>`, which is unique to this run. Never abandon a table for want of a free name.\n\n**On a successful auto-rename, do these in order BEFORE the POST:**\n\n1. **Update `native-app-plan.md`** — `Edit` with `replace_all: true` to swap the old logical name for the new one across the entire `## Data Model` section (Mermaid ER, Reuse\u002FExtend\u002FCreate table, Creation Order, Notes). This catches downstream relationship POSTs in this same Step 5 too.\n2. **Update `## Screens` per-screen specs** — same `replace_all` sweep for any service \u002F data-source references using the old name.\n3. **Append to `memory-bank.md` Collision history** — `\u003Coriginal> → \u003Cnew>` with reason (foreign \u002F reserved \u002F tombstone) and timestamp.\n4. **Update the run-level alias map** — every later metadata payload and plan edit resolves `\u003Coriginal>` to `\u003Cnew>` before use.\n5. **Inform the user — single line, no prompt:**\n   > `→ Collision on \u003Coriginal> (\u003Cforeign|reserved|tombstone>). Renamed to \u003Cnew> and updated plan + memory-bank. Continuing.`\n\nThen proceed with the POST using `\u003Cnew>`.\n\n#### Post-create collision rescue — hidden tombstone \u002F recent delete\n\nIf the Step 5b table POST fails after a 404 preflight with any Dataverse name-collision signature, **do not fail the run**:\n\n- HTTP `400` with code `0x80044363`\n- message contains `schema name` and `not unique`\n- message contains `same name already exists`\n- message contains `object with same name exists in solution`\n\nFirst attempt auto-Extend: compare the plan against that table's attribute snapshot from Step 4 (re-fetch only if it is absent). If ≥50% overlap, switch to Extend path (add missing columns). Otherwise, run the auto-rename probe sequence, update `native-app-plan.md`, `## Screens`, `memory-bank.md`, and the run-level alias map, then retry the table POST exactly once with the resolved name. Print:\n\n> `→ Dataverse still has \u003Coriginal> reserved from a recent delete\u002Fhidden collision. Using \u003Cnew> and continuing.`\n\nIf the retry also returns a collision signature, continue probing the remaining candidates, then the numeric tail, then the run-token form described above.\n\n**On successful POST**, immediately re-GET to capture the server-assigned `MetadataId` and write it to memory-bank (Step 6d updates `.datamodel-manifest.json`; you also append to `memory-bank.md` under \"Created tables\" with the GUID and solution name). This lets future `\u002Fadd-dataverse` runs distinguish \"we own this\" from \"name collision.\"\n\n#### Step 5b — Create \u002F extend\n\nRun this step in two explicit passes:\n\n1. **Pass 1 — tables + ordinary columns:** create every new table with all planned non-lookup columns inline, then extend existing tables with any missing non-lookup columns. Computed formula definitions are never synthesized by this workflow.\n2. **Pass 2 — lookups + relationships:** only after Pass 1 has succeeded for every tier, create the planned `RelationshipDefinitions`. A lookup is created by its relationship and must not appear in a table's initial `Attributes` array.\n\nBoth passes remain strictly sequential. Do not use `$batch` for metadata writes.\n\nFor each `Create` decision, in **tier order** (Tier 0 → Tier 1 → Tier 2 → …), POST a new EntityDefinition. Skip if Step 5a returned a known-self match (idempotent).\n\n> **⚠️ Inline ALL planned columns into the Create POST body — do NOT POST columns individually.**\n>\n> Dataverse accepts all non-lookup columns in the initial table's `Attributes: [...]` array. Inline form: 1 round trip, ~3-8s. Per-column form: N+1 round trips, each ~3-8s. For a 5-column table that's roughly 24s saved on the lock-serialized metadata path. Do not describe this metadata create as transactionally atomic: if the request fails after Dataverse starts processing it, the table or some attributes may remain, which is why the recovery path below exists.\n>\n> **Wrong** (N round trips):\n> ```json\n> { \"SchemaName\": \"...\", \"Attributes\": [{ \u002F* primary only *\u002F }] }\n> \u002F\u002F then 4× POST \u002FAttributes for the rest\n> ```\n>\n> **Right** (1 round trip):\n> ```json\n> {\n>   \"SchemaName\": \"...\",\n>   \"Attributes\": [\n>     { \u002F* primary name *\u002F },\n>     { \u002F* column 2 *\u002F },\n>     { \u002F* column 3 *\u002F },\n>     { \u002F* column 4 *\u002F },\n>     { \u002F* column 5 *\u002F }\n>   ]\n> }\n> ```\n>\n> The per-column POST path remains valid for two cases only: (1) Extend on an existing table, (2) retry-after-partial-failure when Step 5a's pre-flight shows the table exists but some columns don't.\n\n**Solution targeting (HARD):** every Step 5 \u002F 5b POST MUST pass `--solution \u003Cuniquename>` so Dataverse routes the new artifact into our solution rather than the unmanaged default. Read the solution name from `memory-bank.md` Power Platform context (captured in Step 3b). Without this flag, multi-project environments end up with cross-solution leakage and the foreign-collision class of bug returns. The script translates `--solution` to the `MSCRM.SolutionUniqueName` HTTP header.\n\n**Scratch files:** When writing request body JSON to disk (e.g. table definitions, column metadata, relationship payloads), always write to `\u003Cworking_dir>\u002F.tmp\u002F`, never to `\u002Ftmp\u002F`. Keeping request bodies project-local prevents cross-project writes and makes cleanup deterministic. After the per-call root guard, create the folder if it doesn't exist: `mkdir -p .tmp`.\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> POST EntityDefinitions \\\n  --body '\u003Cjson-body-with-all-columns-inline>' \\\n  --solution '\u003Csolution-uniquename-from-memory-bank>' \\\n  --tenant-id '\u003CtenantId-from-resolve-environment>'\n```\n\n**Complete-payload self-check (HARD — do this before the POST, no extra tooling):** re-read the body you just built against the plan and confirm all five statements. If any fails, fix the body and re-check; never POST a partial table and repair it with per-column POSTs.\n\n1. **Every planned ordinary column is present** — String, Memo, Integer, BigInt, Decimal, Money, DateTime, Boolean, Choice, MultiSelect Choice, Image, and File. Count `Attributes[]` and compare with the plan's column count for this table.\n2. **No lookup metadata is inline** — no `LookupAttributeMetadata`, `CustomerAttributeMetadata`, or `OwnerAttributeMetadata`. Those are created by their relationship in Pass 2.\n3. **No deferred or server-owned column is inline** — no primary-id column and no calculated\u002Frollup column (Step 5c owns those).\n4. **Exactly one `IsPrimaryName: true` attribute exists**, and `PrimaryNameAttribute` matches its `SchemaName` (lowercased logical form).\n5. **No duplicate `SchemaName`** in `Attributes[]` (compare case-insensitively).\n\nMicrosoft documents both halves of this contract: [ordinary columns may be included when the table is created](https:\u002F\u002Flearn.microsoft.com\u002Fpower-apps\u002Fdeveloper\u002Fdata-platform\u002Fwebapi\u002Fcreate-update-column-definitions-using-web-api#create-columns), while a [lookup is created with its one-to-many relationship](https:\u002F\u002Flearn.microsoft.com\u002Fpower-apps\u002Fdeveloper\u002Fdata-platform\u002Fwebapi\u002Fcreate-update-entity-relationships-using-web-api#create-a-one-to-many-relationship). `dataverse-request.js` stays the only script in this path — it already owns auth, retry, `--solution` routing, and 401\u002F429 handling.\n\nBody skeleton — **all planned columns inline in `Attributes: [...]`** (this example shows primary + 3 additional; expand the array to fit every column from the plan):\n\n> **⚠️ `IsAvailableOffline` + `ChangeTrackingEnabled` MUST be set to `true` at create time** for any table the app intends to make available offline. Without these two flags the table cannot be added to a `mobileofflineprofile`, and `\u002Fsetup-offline-profile` will have to fix them via a separate metadata PUT (the `\u002Fenable-tables-offline` skill handles that, but it doubles the metadata-lock-serialized round trips). Empirically verified 2026-05-18 in the chanel-rm demo: 7 custom tables created without these flags caused 7 prereq-revert drift entries; fixed by post-hoc enablement. Default these to `true` for all UserOwned tables created by `\u002Fadd-dataverse` unless the user has explicitly opted out of offline support. The flags are no-ops at runtime for apps that don't use offline profiles.\n\n```json\n{\n  \"@odata.type\": \"Microsoft.Dynamics.CRM.EntityMetadata\",\n  \"SchemaName\": \"cr123_jobsite\",\n  \"DisplayName\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"Label\": \"Job Site\", \"LanguageCode\": 1033 }] },\n  \"DisplayCollectionName\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"Label\": \"Job Sites\", \"LanguageCode\": 1033 }] },\n  \"OwnershipType\": \"UserOwned\",\n  \"HasActivities\": false,\n  \"HasNotes\": false,\n  \"IsAvailableOffline\": true,\n  \"ChangeTrackingEnabled\": true,\n  \"PrimaryNameAttribute\": \"cr123_sitename\",\n  \"Attributes\": [\n    {\n      \"@odata.type\": \"Microsoft.Dynamics.CRM.StringAttributeMetadata\",\n      \"SchemaName\": \"cr123_sitename\",\n      \"DisplayName\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"Label\": \"Site Name\", \"LanguageCode\": 1033 }] },\n      \"MaxLength\": 200,\n      \"FormatName\": { \"Value\": \"Text\" },\n      \"RequiredLevel\": { \"Value\": \"ApplicationRequired\" },\n      \"IsPrimaryName\": true\n    },\n    {\n      \"@odata.type\": \"Microsoft.Dynamics.CRM.StringAttributeMetadata\",\n      \"SchemaName\": \"cr123_address\",\n      \"DisplayName\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"Label\": \"Address\", \"LanguageCode\": 1033 }] },\n      \"MaxLength\": 500,\n      \"FormatName\": { \"Value\": \"Text\" },\n      \"RequiredLevel\": { \"Value\": \"None\" }\n    },\n    {\n      \"@odata.type\": \"Microsoft.Dynamics.CRM.IntegerAttributeMetadata\",\n      \"SchemaName\": \"cr123_squarefeet\",\n      \"DisplayName\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"Label\": \"Square Feet\", \"LanguageCode\": 1033 }] },\n      \"RequiredLevel\": { \"Value\": \"None\" },\n      \"MinValue\": 0,\n      \"MaxValue\": 2147483647,\n      \"Format\": \"None\"\n    },\n    {\n      \"@odata.type\": \"Microsoft.Dynamics.CRM.BooleanAttributeMetadata\",\n      \"SchemaName\": \"cr123_active\",\n      \"DisplayName\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"Label\": \"Active\", \"LanguageCode\": 1033 }] },\n      \"RequiredLevel\": { \"Value\": \"None\" },\n      \"DefaultValue\": true,\n      \"OptionSet\": {\n        \"@odata.type\": \"Microsoft.Dynamics.CRM.BooleanOptionSetMetadata\",\n        \"TrueOption\": { \"Value\": 1, \"Label\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"Label\": \"Yes\", \"LanguageCode\": 1033 }] } },\n        \"FalseOption\": { \"Value\": 0, \"Label\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"Label\": \"No\", \"LanguageCode\": 1033 }] } }\n      }\n    }\n  ]\n}\n```\n\nFor each `Extend` decision, POST a new column to the existing table.\n\n> **⚠️ Table-level pre-flight (HARD — required for idempotent re-runs).** Reuse the complete attribute snapshot fetched for this table in Step 4. If the table was discovered only during collision recovery, or no current snapshot exists, fetch all attributes exactly once:\n>\n> ```bash\n> cd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\n> node \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> GET \\\n>   \"EntityDefinitions(LogicalName='\u003Ctable>')\u002FAttributes?\\$select=MetadataId,LogicalName,SchemaName,AttributeType,AttributeTypeName,RequiredLevel,IsManaged,IsCustomizable,IsPrimaryId,IsPrimaryName\" \\\n>   --tenant-id '\u003CtenantId-from-resolve-environment>'\n> ```\n>\n> Build a local `{ lowerCaseLogicalName → { AttributeType, AttributeTypeName, IsManaged, IsCustomizable } }` map and classify **every** planned non-lookup column before issuing any POST. Prefer `AttributeTypeName.Value` when `AttributeType` is `Virtual`, so File and Image columns are not incorrectly treated as compatible generic virtual attributes:\n>\n> | Snapshot result | Action |\n> |---|---|\n> | Name exists and `AttributeType` matches | Skip it and log `↻ \u003Ccolumn> (already exists, skipped)`. |\n> | Name exists and `AttributeType` differs | Dataverse does not allow column-type changes via API, so **auto-rename the planned column** using the same probe sequence (`\u003Ccolumn>v2` → `\u003Ccolumn>v3` → `\u003Ccolumn>2` → …), add it to the alias map, and create it beside the existing one. Log `→ \u003Ccolumn> exists as \u003CexistingType>; created \u003CnewName> as \u003CplannedType> instead.` Never modify or delete the existing column. |\n> | Name is absent | Add it to the ordered missing-column queue. |\n>\n> This single snapshot catches partial creates, corrected re-runs, and network-drop recovery without paying one GET round trip per column.\n\nAfter the complete comparison passes, POST the missing-column queue **one column at a time, sequentially** (no `$batch`; always pass `--solution`):\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> POST \\\n  \"EntityDefinitions(LogicalName='\u003Ctable>')\u002FAttributes\" \\\n  --body '\u003Ccolumn-json>' \\\n  --solution '\u003Csolution-uniquename-from-memory-bank>' \\\n  --tenant-id '\u003CtenantId-from-resolve-environment>'\n```\n\n**Recovery after a partial Create:** do not re-POST the whole `EntityDefinitions` body because the table now exists and Dataverse returns `0x80060888`. Fetch that table's complete attribute snapshot once, run the same local comparison, and sequentially POST only the missing non-lookup columns. This is the only time a table originally classified as Create should use the per-column path.\n\nColumn shapes that have non-obvious gotchas (handle carefully):\n- **Pass-2 barrier (HARD)** — do not create any lookup or relationship while Pass 1 is still creating or extending tables. Accumulate relationship definitions, wait until every table and ordinary column has returned 2xx, then process the relationships sequentially in dependency order.\n- **Lookup** — POST to `\u002FRelationshipDefinitions`, not `\u002FAttributes`.\n\n  > **⚠️ Do NOT improvise the body. Copy the skeleton below verbatim and replace only the placeholders in `\u003C>` brackets.**\n  >\n  > Fields that cause silent failure if added:\n  > - **Do NOT include `ReferencingAttribute`.** Dataverse auto-creates the foreign-key column from `Lookup.SchemaName`. Including it causes `404: Could not find an attribute with specified name` because the column doesn't exist yet at POST time.\n  > - **Do NOT include `Lookup.LogicalName`.** It's read-only metadata; including it returns `400 Bad Request`.\n  > - **Do NOT include `ReferencedAttribute`.** Dataverse resolves the primary key of the referenced entity automatically. The reference is optional and omitting it is the correct default.\n  >\n  > Required fields: `SchemaName`, `ReferencedEntity`, `ReferencingEntity`, `Lookup.{@odata.type, SchemaName, DisplayName, RequiredLevel}`, `AssociatedMenuConfiguration`, `CascadeConfiguration` (including `RollupView`). Anything else is invented — drop it.\n\n  ```json\n  {\n    \"@odata.type\": \"Microsoft.Dynamics.CRM.OneToManyRelationshipMetadata\",\n    \"SchemaName\": \"\u003Cprefix>_\u003CParent>_\u003CChild>\",\n    \"ReferencedEntity\": \"\u003Cparent_table_logical_name>\",\n    \"ReferencingEntity\": \"\u003Cchild_table_logical_name>\",\n    \"Lookup\": {\n      \"@odata.type\": \"Microsoft.Dynamics.CRM.LookupAttributeMetadata\",\n      \"SchemaName\": \"\u003CPrefix>_\u003CParent>Id\",\n      \"DisplayName\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"@odata.type\": \"Microsoft.Dynamics.CRM.LocalizedLabel\", \"Label\": \"\u003CParent Display Name>\", \"LanguageCode\": 1033 }] },\n      \"RequiredLevel\": { \"Value\": \"None\" }\n    },\n    \"AssociatedMenuConfiguration\": { \"Behavior\": \"UseCollectionName\", \"Group\": \"Details\", \"Order\": 10000 },\n    \"CascadeConfiguration\": {\n      \"Assign\": \"NoCascade\",\n      \"Delete\": \"RemoveLink\",\n      \"Merge\": \"NoCascade\",\n      \"Reparent\": \"NoCascade\",\n      \"Share\": \"NoCascade\",\n      \"Unshare\": \"NoCascade\",\n      \"RollupView\": \"NoCascade\"\n    }\n  }\n  ```\n\n  Invocation (apiPath is `RelationshipDefinitions`, body via `--body`, always pass `--solution`):\n\n  ```bash\n  cd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\n  node \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> POST \\\n    RelationshipDefinitions \\\n    --body '\u003Cjson-body-from-skeleton-above>' \\\n    --solution '\u003Csolution-uniquename-from-memory-bank>' \\\n    --tenant-id '\u003CtenantId-from-resolve-environment>'\n  ```\n\n  **Pre-flight the lookup (HARD — required for idempotent re-runs).** A lookup can only pre-exist if the **referencing (child) table** already existed at Step 4, so when the child was created in this run, skip the probe and POST. Otherwise look for the lookup's foreign-key column — the lowercased `Lookup.SchemaName`, e.g. `\u003Cprefix>_\u003Cparent>id` — in that child table's Step 4 attribute snapshot:\n\n  | Snapshot result | Action |\n  |---|---|\n  | Present with `AttributeType: Lookup` | Skip the POST and log `↻ \u003CSchemaName> (relationship already exists, skipped)`. |\n  | Present with any other `AttributeType` | A non-lookup column already owns that name. Auto-rename the lookup's `Lookup.SchemaName` via the probe sequence, record it in the alias map, and POST the relationship with the new name. Never overwrite the existing column. |\n  | Absent | POST the relationship. |\n\n  This costs no extra round trip: the referencing attribute is an ordinary attribute on the child table, so it is already in the snapshot Step 4 fetched. Without this check a re-run POSTs a duplicate relationship and fails the run mid-Pass-2.\n\n- **Many-to-Many (M:N)** — also POST to `\u002FRelationshipDefinitions`, but with `ManyToManyRelationshipMetadata`. Dataverse creates an auto-named intersect table.\n\n  > **⚠️ Do NOT improvise the body.** Required fields: `SchemaName`, `Entity1LogicalName`, `Entity2LogicalName`, `IntersectEntityName`, and the two `AssociatedMenuConfiguration` blocks. Do not include lookup or cascade fields — those are 1:N concepts.\n\n  ```json\n  {\n    \"@odata.type\": \"Microsoft.Dynamics.CRM.ManyToManyRelationshipMetadata\",\n    \"SchemaName\": \"\u003Cprefix>_\u003Ctable1>_\u003Ctable2>\",\n    \"Entity1LogicalName\": \"\u003Ctable1_logical_name>\",\n    \"Entity2LogicalName\": \"\u003Ctable2_logical_name>\",\n    \"IntersectEntityName\": \"\u003Cprefix>_\u003Ctable1>_\u003Ctable2>\",\n    \"Entity1AssociatedMenuConfiguration\": {\n      \"Behavior\": \"UseLabel\",\n      \"Group\": \"Details\",\n      \"Label\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"@odata.type\": \"Microsoft.Dynamics.CRM.LocalizedLabel\", \"Label\": \"\u003CTable2 Plural>\", \"LanguageCode\": 1033 }] },\n      \"Order\": 10000\n    },\n    \"Entity2AssociatedMenuConfiguration\": {\n      \"Behavior\": \"UseLabel\",\n      \"Group\": \"Details\",\n      \"Label\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"@odata.type\": \"Microsoft.Dynamics.CRM.LocalizedLabel\", \"Label\": \"\u003CTable1 Plural>\", \"LanguageCode\": 1033 }] },\n      \"Order\": 10000\n    }\n  }\n  ```\n\n  **Pre-flight M:N:** a relationship can only pre-exist on a table that already existed at Step 4. If **either** endpoint was just created in this run, skip the probe — nothing can be there. Otherwise read `ManyToManyRelationships` from that table's Step 4 snapshot: present → skip (already exists); absent → proceed. Query `RelationshipDefinitions(SchemaName='\u003Cprefix>_\u003Ctable1>_\u003Ctable2>')?$select=SchemaName` only when the snapshot did not cover it.\n\n  **In the generated service:** M:N relationships are queried via the intersect entity name (e.g., `cr123_tag_inspection`) — the SDK does not expose a direct M:N navigation helper; the screen-builder must query the intersect table directly. Flag this in the Step 7 summary if any M:N relationships are created.\n\n- **Column `@odata.type` and required fields — reference table (verified against Dataverse OData API):**\n\n  | Dataverse type | `@odata.type` | Required extra fields |\n  |---|---|---|\n  | Single-line text | `Microsoft.Dynamics.CRM.StringAttributeMetadata` | `MaxLength` (200), `FormatName: { \"Value\": \"Text\" }` — values: `Text`, `Email`, `Url`, `Phone`, `TextArea` |\n  | Multi-line text | `Microsoft.Dynamics.CRM.MemoAttributeMetadata` | `MaxLength` (10000), `Format: \"TextArea\"` |\n  | Whole number | `Microsoft.Dynamics.CRM.IntegerAttributeMetadata` | `MinValue`, `MaxValue`, `Format: \"None\"` |\n  | Decimal | `Microsoft.Dynamics.CRM.DecimalAttributeMetadata` | `MinValue`, `MaxValue`, `Precision` (2) |\n  | Currency (Money) | `Microsoft.Dynamics.CRM.MoneyAttributeMetadata` | `MinValue`, `MaxValue`, `Precision` (2), `PrecisionSource` (2) |\n  | Date\u002FTime | `Microsoft.Dynamics.CRM.DateTimeAttributeMetadata` | `Format: \"DateAndTime\"` or `\"DateOnly\"`, `DateTimeBehavior: { \"Value\": \"UserLocal\" }` |\n  | Boolean | `Microsoft.Dynamics.CRM.BooleanAttributeMetadata` | `DefaultValue`, `OptionSet` with `TrueOption`\u002F`FalseOption` |\n  | Choice (picklist) | `Microsoft.Dynamics.CRM.PicklistAttributeMetadata` | `OptionSet` with `IsGlobal: false`, `OptionSetType: \"Picklist\"`, `Options[]` — **option integer values start at `100000000` and increment by 1** |\n  | Lookup | via `RelationshipDefinitions` — see 1:N skeleton above | — |\n  | Image | `Microsoft.Dynamics.CRM.ImageAttributeMetadata` | `MaxSizeInKB` (default 10240), `CanStoreFullImage` |\n  | File | `Microsoft.Dynamics.CRM.FileAttributeMetadata` | `MaxSizeInKB` |\n\n  **Common mistake:** omitting `FormatName` on String columns and `DateTimeBehavior` on DateTime columns. Both are required — Dataverse rejects the POST without them.\n\n- **Choice (option set)** — set `OptionSet.IsGlobal: false` for local picklists. Full body (option values start at `100000000` and increment by 1):\n\n  ```json\n  {\n    \"@odata.type\": \"Microsoft.Dynamics.CRM.PicklistAttributeMetadata\",\n    \"SchemaName\": \"\u003CPrefix>_\u003CColumnName>\",\n    \"DisplayName\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"@odata.type\": \"Microsoft.Dynamics.CRM.LocalizedLabel\", \"Label\": \"\u003CDisplay Name>\", \"LanguageCode\": 1033 }] },\n    \"RequiredLevel\": { \"Value\": \"None\" },\n    \"OptionSet\": {\n      \"@odata.type\": \"Microsoft.Dynamics.CRM.OptionSetMetadata\",\n      \"IsGlobal\": false,\n      \"OptionSetType\": \"Picklist\",\n      \"Options\": [\n        { \"Value\": 100000000, \"Label\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"@odata.type\": \"Microsoft.Dynamics.CRM.LocalizedLabel\", \"Label\": \"Option 1\", \"LanguageCode\": 1033 }] } },\n        { \"Value\": 100000001, \"Label\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"@odata.type\": \"Microsoft.Dynamics.CRM.LocalizedLabel\", \"Label\": \"Option 2\", \"LanguageCode\": 1033 }] } }\n      ]\n    }\n  }\n  ```\n- **Image** — `@odata.type: Microsoft.Dynamics.CRM.ImageAttributeMetadata`,\n  `MaxSizeInKB` and `CanStoreFullImage`. Dataverse always reports\n  `MaxHeight: 144` and `MaxWidth: 144` for the generated thumbnail; those values\n  cannot be changed and must not be used as full-image dimensions. Set\n  `CanStoreFullImage: true` when users must inspect or download the retained\n  full-size image. Updating this setting requires retrieving the complete\n  current `ImageAttributeMetadata`, changing the writable property, sending a\n  full-definition `PUT`, and publishing customizations.\n- **File** — `@odata.type: Microsoft.Dynamics.CRM.FileAttributeMetadata`, `MaxSizeInKB` required\n\nIf the column type is not a simple string\u002Fint\u002Fboolean, surface a one-line confirmation to the user before posting.\n\nAfter all mutations, re-run the existing-tables query (Step 4) to confirm everything landed.\n\n### Step 5c — Enforce the supported computed-column boundary\n\nDataverse metadata exposes `FormulaDefinition`, but Microsoft explicitly does\nnot support defining calculated, rollup, or formula expressions through code.\nLegacy workflow XAML and generated formula serialization must not be synthesized.\n\n- Do not invoke `create-calculated-column.js`; it is a fail-closed guard for\n  legacy callers.\n- Cross-entity fields must use a supported formatted lookup annotation or\n  bounded chained fetch as documented in `data-performance.md`.\n- For a hot list field that cannot use either path, mark it\n  `external-projection-required` and omit it from this mutation run. The user\n  may create a formula column in Power Apps or supply another server-owned\n  projection outside this PR, then rerun Step 4a to validate and reuse it.\n- Never create an ordinary copied field without an explicit refresh owner.\n\nReference:\nhttps:\u002F\u002Flearn.microsoft.com\u002Fpower-apps\u002Fdeveloper\u002Fdata-platform\u002Fspecialized-columns\n\n### Step 5d — Create alternate keys for unique business identifiers\n\n**Print before starting:**\n> \"→ Creating alternate keys for columns marked unique in the data model (one HTTP call per key). Skip if no unique columns are planned.\"\n\n**Run condition:** the `## Data Model` section marks a non-primary column as unique \u002F alternate key \u002F natural key. Common examples: QR Code Value, SKU, external ID, employee number, asset tag. Skip primary IDs and skip columns whose type Dataverse cannot index as an alternate key (file\u002Fimage, memo\u002Flong text, multi-select choice, calculated\u002Frollup, customer\u002Fowner lookups).\n\n**Ordering:** run after the target table and target columns exist, and before Step 6b publish. Alternate-key index activation is asynchronous; creation may return success while the key status is `Pending`.\n\n**Do NOT use the `CreateEntityKey` action route.** In practice it can return 404 depending on route shape \u002F environment. The reliable metadata route is POSTing to the table's `Keys` navigation collection:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> POST \\\n  \"EntityDefinitions(LogicalName='\u003Ctable>')\u002FKeys\" \\\n  --body '\u003Centity-key-json>' \\\n  --solution '\u003Csolution-uniquename-from-memory-bank>' \\\n  --tenant-id '\u003CtenantId-from-resolve-environment>'\n```\n\nBody skeleton:\n\n```json\n{\n  \"@odata.type\": \"Microsoft.Dynamics.CRM.EntityKeyMetadata\",\n  \"SchemaName\": \"\u003Cprefix>_\u003Ctable>_\u003Ccolumn>_key\",\n  \"DisplayName\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"Label\": \"\u003CColumn display> Key\", \"LanguageCode\": 1033 }] },\n  \"KeyAttributes\": [\"\u003Ccolumn_logical_name>\"]\n}\n```\n\n**Pre-flight each key before POST** so re-runs are idempotent. A key can only pre-exist on a table that already existed at Step 4, so for a table created in this run, skip straight to the POST. Otherwise read `Keys` from that table's Step 4 snapshot. Query it directly only when the snapshot did not cover that table:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> GET \\\n  \"EntityDefinitions(LogicalName='\u003Ctable>')?\\$select=LogicalName&\\$expand=Keys(\\$select=SchemaName,KeyAttributes,EntityKeyIndexStatus)\" \\\n  --tenant-id '\u003CtenantId-from-resolve-environment>'\n```\n\n| Existing key state | Action |\n|---|---|\n| Same `SchemaName` or same `KeyAttributes` exists with `Active` \u002F `Pending` | Skip POST; record the key in `.datamodel-manifest.json`. |\n| Same `SchemaName` exists with `Failed` | Surface the failure and stop; Dataverse requires deleting\u002Frecreating the key manually or changing the planned key name. |\n| No matching key | POST to `EntityDefinitions(LogicalName='\u003Ctable>')\u002FKeys`. |\n\n**After POST:** a `204` response is success. Re-query the `Keys` expand above and capture `EntityKeyIndexStatus`. If it is `Pending`, continue the scaffold but add a memory-bank follow-up: `alternate key \u003Cschema> pending index activation`. Do not rely on duplicate enforcement in manual tests until the status reaches `Active`.\n\nAdd alternate keys to `.datamodel-manifest.json` for the table:\n\n```json\n\"alternateKeys\": [\n  { \"schemaName\": \"cr123_item_code_key\", \"keyAttributes\": [\"cr123_code\"], \"indexStatus\": \"Pending\" }\n]\n```\n\n### Step 6 — Add data sources\n\n**Telemetry checkpoint: `generate_dataverse_data_sources`**\n\n**Print before starting:**\n> \"→ Generating TypeScript services for \u003CN> tables via `pa app add data-source` (sequential). Print '✓ \u003Ctable>Service.ts' after each.\"\n\nWhen `\u003Coperation_manifest_mode> = valid`, set `SERVICE_REQUIRED_TABLES` from\n`service.requiredTables[].logicalName`. Keep the manifest's resolved adapted\nnames and exclude only explicit deferred rows. Service generation remains\nsequential outside BATCH-METADATA.\n\nFor each table in `SERVICE_REQUIRED_TABLES` (regardless of reuse\u002Fextend\u002Fcreate),\nverify its registered service before deciding whether generation is needed.\nPreserve verified unchanged services outside the approved delta. For an approved\nrefresh or a service affected by this run's approved schema changes, use\n[Refresh a retained source](..\u002F..\u002Fshared\u002Freferences\u002Fdata-source-removal.md#refresh-a-retained-source)\nwith the exact existing registration. Only an approved missing binding takes\nthe add command below. A required unregistered service outside approved scope\nreturns `NEEDS_CONTEXT`, not permission to register unrelated tables.\nDo not derive the service list from Creation Order alone because reused tables\nare intentionally absent from creation tiers. The CLI reads the environment ID\nfrom `power.config.json`; pass the environment URL resolved earlier in the skill:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\n$PA app add data-source --connector dataverse --org-url \u003CenvUrl> --table \u003Ctable-logical-name>\n```\n\nRun **one at a time — sequentially**, not in parallel. The Power Apps CLI writes `src\u002Fgenerated\u002FconnectorSchemas.ts` and other generated files non-atomically; concurrent invocations corrupt them.\n\nAfter generation, verify the output created by\n`pa app add data-source` rather than guessing a JSON path or service\nfilename. The command writes Dataverse configuration under the literal\n`databaseReferences[\"default.cds\"].dataSources` key and derives service\nfilenames from each entry's `entitySetName`, which may differ from the table\nlogical name. The verifier also accepts the legacy nested\n`databaseReferences.default.cds` shape:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fverify-dataverse-services.js\" \\\n  --project-root '\u003Cworking_dir>' \\\n  --manifest \"$OPERATION_MANIFEST\"\n```\n\nFor the standalone fallback path without a manifest, pass the resolved service\nlist as `--tables \"\u003Ccomma-separated-logical-names>\"`. If any reused or custom\ntable is missing from config, lacks `entitySetName`, or has no matching generated\nservice, STOP before screen generation:\n\n```text\nBLOCKED: required Dataverse service missing for \u003Clogical-name>. Schema action=\u003Creuse|extend|create>; app usage=\u003Cscreens\u002Fhooks that require it>.\n```\n\n### Step 6b — Publish customizations\n\n**Telemetry checkpoint: `publish_dataverse_customizations`**\n\nWhen `\u003Coperation_manifest_mode> = valid`, skip this step: the validated\nmanifest's final `publish` phase already ran and its pending checkpoint was\ndeleted, or the manifest proved there were zero metadata writes and no pending\npublish retry.\n\n**Print before starting:**\n> \"→ Publishing customizations (PublishXml) so new tables\u002Fcolumns become queryable. ~5–20 seconds.\"\n\nOnly after **every** Step 5 metadata POST and **every** Step 6 `pa app add data-source` has returned successfully, publish so the new tables and columns are available to the runtime. `PublishXml` takes the same exclusive metadata lock as the create\u002Fextend calls — do not run it concurrently with anything from Steps 5 or 6.\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> POST \\\n  \"PublishXml\" \\\n  --body \"{\\\"ParameterXml\\\":\\\"\u003Cimportexportxml>\u003Centities>\u003Centity>cr123_table1\u003C\u002Fentity>\u003Centity>cr123_table2\u003C\u002Fentity>\u003C\u002Fentities>\u003C\u002Fimportexportxml>\\\"}\" \\\n  --tenant-id '\u003CtenantId-from-resolve-environment>'\n```\n\nBuild the entity list from all tables that were **created or extended** in Steps 4–5. Skip reused-as-is tables — they don't need republishing.\n\nIf the publish call returns a non-2xx status, report the error and stop — do not proceed. The user must resolve before the tables are usable.\n\nAfter a 2xx publish, invalidate the planning-only inventory cache before\nverification so a later planning run cannot reuse pre-publication inventory:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nif ! node \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-inventory-cache.js\" \\\n  --file '\u003Cworking_dir>\u002F.tmp\u002Fdataverse-inventory-cache.json' --invalidate; then\n  printf 'NEEDS_RECOVERY: dataverse-inventory-cache\\n' >&2\n  exit 2\nfi\n```\n\nApply the same cache-invalidation recovery rule from the validated publish path\nabove. Continue to Step 6c only after invalidation exits `0`.\n\n### Step 6c — Verify tables exist\n\n**Telemetry checkpoint: `verify_dataverse_schema`**\n\nConfirm every table in `SERVICE_REQUIRED_TABLES`, including reused-as-is tables,\nplus any created or extended table, is queryable after publication when needed.\nUse the resolved effective names from the approved operation manifest or the\nstandalone path's verified service list, not Creation Order alone. Use **one**\nfiltered query for the bounded set, not one request per table:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> GET \\\n  \"EntityDefinitions?\\$select=LogicalName,DisplayName&\\$filter=LogicalName eq '\u003Ctable1>' or LogicalName eq '\u003Ctable2>'\" \\\n  --tenant-id '\u003CtenantId-from-resolve-environment>'\n```\n\n- **Every expected name present in `value[]`** → confirmed.\n- **Any expected name missing** → schema\u002Fservice verification is incomplete.\n  Return to the owning reconciliation step; do not mark that table materialized\n  or invent a new table to replace a missing reuse target.\n\n### Step 6d — Write `.datamodel-manifest.json`\n\nAfter schema verification in Step 6c and generated-service verification in\nStep 6 both pass, write the manifest to the project root using the `Write`\ntool. This is the application's verified table inventory, not a schema-write\nlog; a reuse-only app must still have a populated manifest:\n\n```json\n{\n  \"environmentUrl\": \"\u003CenvUrl>\",\n  \"generatedAt\": \"\u003CISO timestamp>\",\n  \"tables\": [\n    {\n      \"logicalName\": \"cr123_jobsite\",\n      \"displayName\": \"Job Site\",\n      \"status\": \"new\",\n      \"metadataId\": \"\u003Cserver-assigned GUID from Step 5a re-GET>\",\n      \"solution\": \"\u003Csolution unique name, e.g. PowerAppsDefault>\",\n      \"columns\": [\n        { \"logicalName\": \"cr123_sitename\", \"type\": \"String\" },\n        { \"logicalName\": \"cr123_address\",  \"type\": \"String\" }\n      ]\n    }\n  ]\n}\n```\n\n`metadataId` and `solution` are required for `status: \"new\"` or `\"extended\"` entries — they're how Step 5a distinguishes \"we own this on a re-run\" from \"name collision.\" Reused tables can omit both.\n\nInclude every service-required table confirmed in Step 6c, including tables\nreused with no schema changes, plus verified new\u002Fextended tables. Use\n`status: \"reused\"` for unchanged existing tables; retain `\"new\"` and\n`\"extended\"` only for their actual outcomes. Preserve effective adapted names,\nentity-set names, and verified column\u002Frelationship\u002Fkey facts from reconciliation\nand any post-write metadata reads. Exclude deferred or unverified tables.\n\nBefore returning, check that every `SERVICE_REQUIRED_TABLES` name occurs in the\nmanifest and has verified generated service output. A partial or empty manifest\nfrom an earlier run is repaired by repeating Step 6's read-only service verifier\nand Steps 6c–6d, not by replaying successful schema writes. Recording a reused\ntable does not authorize a metadata POST or republish; the existing sample-data\nrecord-count checks and standard-system-table exclusions still apply.\n\nDuring an edit with pending removals, preserve retiring entries until the\nremoval branch verifies that their app bindings\u002Fservices are gone. The owner\nthen reconciles the final inventory; a shortened plan alone is not cleanup.\nReturn the actual created\u002Fextended\u002Freused table sets for this invocation to the\nuser or current owner; historical manifest status is not evidence of creation\nin this invocation. Seeding must use an\nexplicit approved allowlist excluding retirements, never this transitional\ninventory as its insertion scope.\n\n### Step 7 — Inspect generated files\n\n```text\nGlob: \u003Cworking_dir>\u002Fsrc\u002Fgenerated\u002Fservices\u002F*Service.ts\nGlob: \u003Cworking_dir>\u002Fsrc\u002Fgenerated\u002Fmodels\u002F*Model.ts\n```\n\nFor each table, check the generated service exposes the expected methods:\n\n```text\nGrep pattern=\"async (create|getAll|getById|update|delete|upload|downloadFile|downloadImage)\" path=\"\u003Cworking_dir>\u002Fsrc\u002Fgenerated\u002Fservices\u002F\u003CTable>Service.ts\"\n```\n\nIf the table has file or image columns, confirm the service includes `upload`, `downloadFile`, `downloadImage`, `deleteFileOrImage` — and the model exposes `\u003CTable>FileColumnName` \u002F `\u003CTable>ImageColumnName` union types.\n\n**File\u002Fimage column UI controls:** When a generated table has File or Image columns, note this in the summary so screen-builders apply the host controls from `@microsoft\u002Fpower-apps-native-host`:\n- **File columns** → `\u003CFilePicker>`; upload bytes separately via the generated service's upload method after the main create\u002Fupdate.\n- **Image columns** → `\u003CImagePicker>`; capture `PickedImageInfo` via `onImageChange` and persist through generated `upload(...)` after the main create\u002Fupdate.\n- **Read\u002Fview flows** → use generated `downloadFile(...)` \u002F `downloadImage(...)` helpers for existing attachments\u002Fpreviews.\n\nFull usage pattern and the native-wrapper boundary live in [`\u002Fadd-native`](..\u002Fadd-native\u002FSKILL.md#fileimage-picker-ownership); screen-builder keeps only the concise JSX enforcement rule.\n\n**PDF\u002Fsignature artifact schema guidance:** If the approved plan mentions generated PDFs, PDF evidence packets, approvals, signatures, sign-off, ink, or drawings, preserve the storage decision in the Dataverse model instead of defaulting to text fields.\n\n| User need | Dataverse shape | Write pattern |\n|---|---|---|\n| Generated PDF report that must be retained | File column on the parent record, or child Evidence\u002FAttachment table with a File column | Create\u002Fupdate parent row first, then call generated `Service.upload(parentId, '\u003CfileColumn>', file)` |\n| Generated PDF report that is only transient | No Dataverse column required | Generate locally with `expo-print` only when present; share with `expo-sharing` only when present; do not route local URI to native PDF viewer |\n| Captured signature\u002Fsign-off image | Image column when the latest signature belongs on the parent row | Strip `data:image\u002Fpng;base64,` if the generated service expects raw base64, then include image payload in the update body |\n| Multiple signatures, sketches, evidence images, or audit attachments | Child Evidence\u002FAttachment table with Image\u002FFile columns and lookup to parent | Create child row first, then include Image payload or upload File bytes through generated service helpers |\n\nSignature image normalization example:\n\n```ts\nconst signatureBase64 = signatureDataUri.replace(\u002F^data:image\\\u002Fpng;base64,\u002F, '');\nconst result = await Cr123_approvalService.update(approvalId, {\n  cr123_signatureimage: signatureBase64,\n  cr123_signedat: new Date().toISOString(),\n});\n\nif (!result.success) {\n  throw new Error(result.error?.message ?? 'Signature image was not saved.');\n}\n```\n\nFile upload after parent row exists example:\n\n```ts\nconst save = await Cr123_inspectionService.update(inspectionId, {\n  cr123_reportgeneratedat: new Date().toISOString(),\n});\n\nif (!save.success) {\n  throw new Error(save.error?.message ?? 'Inspection was not saved.');\n}\n\nconst upload = await Cr123_inspectionService.upload(inspectionId, 'cr123_reportfile', reportFile);\n\nif (!upload.success) {\n  throw new Error(upload.error?.message ?? 'Inspection report was not uploaded.');\n}\n```\n\n### Step 8 — Type-check\n\n**Telemetry checkpoint: `validate_dataverse_integration`**\n\n**Print before starting:**\n> \"→ Regenerating connector schemas + running tsc to verify generated services compile (~15–30 seconds).\"\n\n`pa app add data-source` (Step 5) wrote new files into `.power\u002Fschemas\u002F\u003Cconnector>\u002F`. The `connectorSchemas.ts` consumed by `app\u002F_layout.tsx` is now stale — regenerate it before type-checking, otherwise the new tables won't be wired into the runtime schema map and `tsc` will pass against an out-of-date snapshot:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnpm run generate-schemas\nnpx --no-install tsc --noEmit\n```\n\nFix any errors. Common: missing peer dependencies — `npx --no-install expo install \u003Cpackage>`.\n\n### Step 8.5 — Offline profile reconciliation\n\nA schema change here (new table or new column) can leave an existing Mobile Offline Profile behind — new tables never sync to devices and new columns come down blank. Reconcile the profile with what you just created.\n\n**Skip this step only for a valid scoped orchestrator handoff with\n`--skip-planning`** whose owner explicitly takes responsibility for offline\nreconciliation, as `\u002Fcreate-mobile-app` and `\u002Fsetup-datamodel` do in their existing\nflows. Return the verified delta to that owner without double-prompting.\nThe flag alone must not suppress standalone reconciliation.\n\nOtherwise (manual `\u002Fadd-dataverse`), run the local, no-network delta check:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Foffline-profile-delta.js\"\n```\n\nBranch on the JSON `status` per [offline-profile-reconciliation.md](${PLUGIN_ROOT}\u002Fshared\u002Freferences\u002Foffline-profile-reconciliation.md):\n\n| `status` | Action |\n|---|---|\n| `no-manifest` \u002F `no-profile` \u002F `in-sync` | Continue to Step 9 silently. For `no-profile` (no offline profile exists) do not nag — the app may not use offline. |\n| `error` | `offline-profile.json` is unreadable — the script prints `status: error` and **exits non-zero**. Do NOT treat this as an `\u002Fadd-dataverse` failure (the tables are already created): surface the `error` string, **skip reconciliation** (never drive the update workflows against a corrupt file), and finish with `DONE_WITH_CONCERNS` telling the user to fix `offline-profile.json`. |\n| `delta` | Prompt the user (one `AskUserQuestion`, default = update now) to add the missing tables \u002F new columns. Use the reconciliation reference's **Scoped helper handoffs** with `orchestrator: add-dataverse`, the absolute `working_dir`, `phase: implementation`, and the approved environment\u002Fprofile\u002Ftable delta. For `missingTables[]`, read and execute `${PLUGIN_ROOT}\u002Fskills\u002Fadd-table-to-offline-profile\u002FSKILL.md` with `--working-dir '\u003Cworking_dir>' --table \u003Ct>`; for `tablesWithNewColumns[]`, read and execute `${PLUGIN_ROOT}\u002Fskills\u002Fedit-offline-profile\u002FSKILL.md` with those arguments plus `--columns add:\u003CnewColumns>`. Re-run the delta check using the reference's failure dispatch; it should read `in-sync`. |\n\n### Step 9 — Summary\n\n```\n✅ Dataverse added\n─────────────────────────────────────────────\nEnvironment   : \u003CenvUrl>\nTables reused : \u003Clist>\nTables extended: \u003Clist (columns added)>\nTables created : \u003Clist (in tier order)>\nAdapted       : \u003Crenamed table\u002Fcolumn → new name, and why. Omit the line if none.>\nDeferred      : \u003Citems left out of this run and why, e.g. \"cr123_note (target not customizable)\". Omit the line if none.>\nOperation manifest: \u003Cvalidated path, operation count, or \"standalone fallback\">\n\nGenerated services:\n  src\u002Fgenerated\u002Fservices\u002F\u003CTable>Service.ts × N\nGenerated models:\n  src\u002Fgenerated\u002Fmodels\u002F\u003CTable>Model.ts × N\n\nType-check: PASS\n\nSample usage:\n\n  import { Cr123_jobsiteService } from '..\u002F..\u002Fsrc\u002Fgenerated\u002Fservices\u002FCr123_jobsiteService';\n\n  const result = await Cr123_jobsiteService.getAll({\n    select: ['cr123_sitename', 'cr123_address'],\n    filter: 'statecode eq 0',\n    orderBy: ['cr123_sitename asc'],\n    top: 50,\n  });\n  const sites = result.data ?? [];\n\n⚠️  First call triggers Dataverse OAuth consent via the native player's\n    `\u003Cscheme>:\u002F\u002Foauth-callback` deep link.\n\nNext:\n  \u002Fadd-sample-data        # Seed each new table with 5-10 realistic rows so the\n                          # app's home screen shows real-looking data on first launch.\n─────────────────────────────────────────────\n```\n\nAfter printing the summary, **offer one-click sample-data seeding** — but only when invoked manually (not from `\u002Fcreate-mobile-app`, which handles this in its own Step 8.5).\n\n- **For a valid scoped orchestrator handoff with `--skip-planning`**: skip the\n  prompt. The orchestrator invokes `\u002Fadd-sample-data` separately.\n- **Otherwise (manual invocation)**, propose exact verified, non-retiring seed\n  targets from this operation and a count policy; do not select every table\n  just because it appears in the manifest. Ask:\n\n  > \"Seed these \u003CN> tables with sample records using the proposed counts? (yes \u002F no)\"\n\n  Only an explicit yes approves seeding. On no\u002Fcancel\u002Fdismissal, stop without\n  inserts; empty input is not consent. On yes, invoke `\u002Fadd-sample-data` with the\n  same absolute `--working-dir`, `--tables \"\u003Capproved-logical-names>\"`, and\n  `--exclude-tables \"\u003Cretiring-logical-names-or-empty>\"`. Carry the specific\n  approval forward; do not broaden it if lookups need additional parents.\n\n## Key Rules\n\n- **Always** use generated services (e.g., `Cr123_jobsiteService.getAll()`) — never `fetch` \u002F `axios` directly.\n- Result data lives at `result.data`, not `result` itself.\n- Don't edit files under `src\u002Fgenerated\u002F` — they are regenerated on every `pa app add data-source`.\n- Picklist (Choice) fields, virtual fields, lookups, and file\u002Fimage columns each have non-obvious gotchas. Keep `references\u002Fdataverse-reference.md` aligned with this skill.\n- A valid operation manifest removes repeated agent reconciliation; it does not\n  change Dataverse's serialized metadata-lock latency. Real matched A\u002FB runs\n  remain required before claiming an end-to-end timing range.\n- **When a Dataverse Web API behavior is uncertain (lookup write syntax, `$expand` nav property names, choice column shape, batch semantics, error format), query the `microsoft-learn` MCP server before guessing.** See [shared\u002Fshared-instructions.md → Microsoft Learn MCP](..\u002F..\u002Fshared\u002Fshared-instructions.md#microsoft-learn-mcp-authoritative-microsoft-docs). Guessed Dataverse syntax silently 400s.\n\n## Reference\n\n- [`scripts\u002Fdataverse-request.js`](..\u002F..\u002Fscripts\u002Fdataverse-request.js) — bundled in this plugin\n- [`scripts\u002Fbuild-dataverse-operation-manifest.js`](..\u002F..\u002Fscripts\u002Fbuild-dataverse-operation-manifest.js) — deterministic contract\u002Fsnapshot reconciliation, binding, validation, and ordered BATCH-METADATA phase generation\n- [shared\u002Freferences\u002Foffline-profile-reconciliation.md](..\u002F..\u002Fshared\u002Freferences\u002Foffline-profile-reconciliation.md) — Step 8.5 offline delta check + reconciliation flow\n","> **Plugin check**: Run `node \"${PLUGIN_ROOT}\u002Fscripts\u002Fcheck-version.js\"` - if it outputs a message, show it to the user before proceeding.\n\n**? Shared instructions: [shared-instructions.md](${PLUGIN_ROOT}\u002Fshared\u002Fshared-instructions.md)** — read first.\n\n# Add Dataverse\n\n**App root:** before any project read or command, execute\n[app-working-directory.md](${PLUGIN_ROOT}\u002Fshared\u002Freferences\u002Fapp-working-directory.md).\nUse its resolved absolute `working_dir` for every shell call and file tool,\nincluding referenced commands and delegated skills; never inherit a prior `cd`.\n\n**Entry routing:** use the shared [App feature entry points](..\u002F..\u002Fshared\u002Fshared-instructions.md#app-feature-entry-points)\npreflight before the workflow below.\n\n**Invocation scope:** follow [Data-source invocation scope](..\u002F..\u002Fshared\u002Fshared-instructions.md#data-source-invocation-scope)\nbefore the workflow below. Standalone calls use this skill's own data-delta\napproval and verification; do not start a full-app plan or screen workflow.\n\nAn existing plan alone does not mean it includes or approves the new request;\nnever replay the old Data Model instead of resolving the requested delta.\n\n**Removal branch:** after resolving invocation scope, `--remove` or an approved app-binding\nremoval executes\n[data-source-removal.md](..\u002F..\u002Fshared\u002Freferences\u002Fdata-source-removal.md), then\nreturns without entering Steps 1-9. Removing a plan row is not implemented by\nre-running table creation or by deleting Dataverse metadata. For a mixed edit,\nthe owner adds\u002Frefreshes first and invokes removal separately after consumer edits.\n\n**Refresh branch:** after resolving invocation scope, `--refresh` or an approved service-only\nrefresh executes [Refresh a retained source](..\u002F..\u002Fshared\u002Freferences\u002Fdata-source-removal.md#refresh-a-retained-source)\nand returns before Steps 1-9. Preserve the exact `--data-source-name` and approved\nbinding identity. Do not replay schema writes, publish, or run `add-data-source`.\n\nTwo paths:\n\n- **Existing tables only** — resolve the current scope in Steps 1–2, then skip schema writes and use Step 6 to add only missing approved bindings or refresh approved retained sources.\n- **New \u002F extended tables** — full workflow with Web API mutations in dependency order\n\n## Workflow\n\n1. Verify project & auth → 2. Resolve plan\u002Foperation manifest → 3. Setup Dataverse Web API auth → 4. Validate manifest or reconcile live metadata → 5. Execute sequential metadata phases → 6. Add data sources → 6b. Publish fallback customizations → 6c. Verify tables → 6d. Write manifest → 7. Inspect generated files → 8. Type-check → 8.5. Offline profile reconciliation → 9. Summary\n\n---\n\n### Step 1 — Verify project & auth\n\nConfirm Power Apps mobile app:\n\nFor `--plan-only` or a planning-phase handoff, check the app files read-only and\nuse the shared proposal-only environment-context rule. Use the same non-persisting\nlookup before approval in a normal invocation. Incomplete or conflicting context\nreturns `NEEDS_CONTEXT` after read-only recovery; never remove the safety flags\nor redirect output into configuration to make planning succeed.\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nif [ ! -f power.config.json ] || [ ! -f app.config.js ]; then\n  echo \"BLOCKED: working_dir is not an initialized app\" >&2\n  exit 1\nfi\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fresolve-environment.js\" \"\u003Cselected-environment-id>\" --no-cache --require-tenant\n```\n\nRead `\u003Cselected-environment-id>` from this app's `power.config.json`. Capture the\n**environment URL** (for example `https:\u002F\u002Fcontoso.crm.dynamics.com`),\n**environment ID**, and **tenant ID** from the resolver for Step 3. Use the\nowner's absolute `working_dir` for every command and artifact.\n\n### Step 2 — Resolve plan\n\n**Telemetry checkpoint: `resolve_dataverse_schema_plan`**\n\nLook for `native-app-plan.md` in the project root:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\ntest -f native-app-plan.md\n```\n\n**Resolve the current request before consuming an existing plan.** A direct\nstandalone invocation must compare its requested tables\u002Fcolumns\u002Fservice\nchanges with the existing plan and read-only live evidence. Present and approve\nthat exact delta at Step 2.7 after shared planning validation, then save only\nthe accepted plan changes before implementation.\nIf the request adds nothing, verify the existing outcome and report a no-op;\ndo not apply other pending rows. If intent or scope is missing, ask or return\n`NEEDS_CONTEXT` without mutation. An explicit request to apply the whole existing\nplan still requires approval of the reconciled operation set.\nFor an approved child call, use only the supplied current `approved_scope`.\nIn either case, `--plan-only` returns the proposal and STOPs before plan saving,\napproval-receipt creation, service generation, or Steps 3–9; a planning-phase\ncaller has the same proposal-only boundary.\n\nBefore reading plan content, inspect `$ARGUMENTS` for the five fast-path\nartifact flags in Step 2a. When all are present, only confirm\n`native-app-plan.md` exists for hash validation; do not parse its Data Model\nsection or build operations\u002Fservice lists from Markdown.\n\nFor all non-fast-path Dataverse proposals from `\u002Fsetup-datamodel`, `\u002Fedit-app`, or\na standalone request, including an existing-plan delta, read and execute\n[dataverse-change-planning.md](..\u002F..\u002Fshared\u002Freferences\u002Fdataverse-change-planning.md).\nUse its scoped evidence\u002Fcontract for the diagram, architect, or inline path.\nAn approved child with `planning_snapshot`, `architect_evidence`, and\n`schema_contract` instead enters Step 2b to verify that supplied scope; it must\nnot rediscover or re-approve it. New-binding requests use this planning path;\nservice-only refreshes and retirements have already returned from their branches.\n\nOlder callers may not supply the new structured handoff. Do not assume compact\nevidence or complete approval from the caller's name. Without a complete approved\nhandoff, resolve the current requested delta and use this leaf's normal proposal\nand Step 2.7 approval path before execution. Resolve required invocation context\nfirst; a missing or conflicting child root still returns `NEEDS_CONTEXT` before\nproject access. Never silently replay the saved plan or treat `--skip-planning`\nas consent.\n\nAn approved legacy child without compact-planning artifacts keeps the supported\nMarkdown\u002Flive-reconciliation path below only when its current request, absolute\nroot, implementation phase, and exact approved delta are all established.\nNever use this legacy path to bypass a partial or invalid compact-planning\nhandoff, or to skip setup\u002Fstandalone proposal validation.\n\n**Legacy input only, if present and `\u003Coperation_manifest_mode> = fallback`\nwithout a shared scoped contract:** read the\n`## Data Model` section. Extract:\n- The target reconciliation table (`reuse` \u002F `extend` \u002F `create` \u002F `adapt` \u002F `defer` decisions and evidence)\n- The Mermaid ER diagram (informational)\n- The \"Creation Order\" tier list\n- Every table referenced by `## Screens`, identity resolution, related-entity fields, forms, dashboards, or shared hooks, including standard reused tables such as `systemuser`, `contact`, and `account`\n\nFor legacy callers without scoped planning artifacts, build\n`SERVICE_REQUIRED_TABLES` as the union of:\n1. every non-deferred row in Target Reconciliation (`reuse`, `extend`, `create`, or `adapt`);\n2. every table in Creation Order;\n3. every table named by screen\u002Fhook data requirements.\n\nKeep this full service inventory as retained context. Only the current approved\ndelta and its necessary dependencies enter live schema reconciliation. With a\ncomplete approved legacy handoff, proceed to Step 3 after this initialization;\ndo not repeat proposal Steps 2.5–2.7 or require new compact artifacts from that\nowner. A planning-phase caller still returns without implementation.\n\nFor the shared scoped path, use the normalized contract's non-deferred\n`serviceRequired` declarations instead. Keep the broader existing-app service\ninventory as retained context, not a registration or mutation work list.\n\n**Hard rule:** `reuse` means \"do not mutate schema\"; it does **not** mean \"skip generated service.\" If app code reads or writes a reused table, that table must be in `SERVICE_REQUIRED_TABLES`.\n\nCarry forward any `adapt` (auto-renamed) and `defer` (out-of-scope this run) decisions with their recorded reasons, and apply the alias map to every name you use. A data-modelling conflict never halts this skill — it resolves to `adapt` or `defer` and is reported in Step 9.\nThis classification belongs to planning: after approval, a changed decision\nreturns to the owner for revision\u002Fapproval before any write.\n\nFor a scoped handoff, restrict schema writes to the exact `approved_scope` delta;\nunaffected plan rows are context, not permission to replay their mutations.\nApply the same restriction to the newly approved standalone request delta.\nRetain the full required-service set for existing screens. If reconciliation\nwould change an approved name, storage target, or screen contract, return the\nproposed adaptation to the owner (or standalone user) before writing. Report\ndependent consumer changes for separate approval; do not silently rename\nunderneath the app or treat data approval as permission to edit screens.\n\n**If absent:** check `$ARGUMENTS` for diagram hints (`*.png`, `*.jpg`, `*.jpeg` filename, `erDiagram` keyword, `||--o{` cardinality syntax). \n\n- **Diagram hint present** → Path A (Step 2.5).\n- **No hint AND `$ARGUMENTS` describes what the app does** (the typical case) → silently take Path B (Step 2.6 — spawn architect). No prompt.\n- **No hint AND `$ARGUMENTS` is empty \u002F non-descriptive** → only then prompt with `AskUserQuestion`:\n\n  > \"How would you like to define the data model?\n  > (a) I have an existing ER diagram to upload (PNG\u002FJPG path, Mermaid syntax, or text description)\n  > (b) Let the data-model-architect agent analyze and propose one (default)\n  > (c) Cancel — I'll plan it elsewhere first\"\n\n  Recommend (b), but wait for an explicit answer. Empty input is not approval;\n  cancellation stops the workflow without planning or mutation. A supplied\n  description may select the read-only architect path, not approve its result.\n\nWith an existing plan and a new request, take the same diagram or architect\u002F\ninline proposal path for only that delta, then Step 2.7. Do not skip validation\nbecause a saved plan exists.\n\n#### Step 2a — Approved operation-manifest fast path\n\nWhen `$ARGUMENTS` supplies all five paths below, record\n`\u003Coperation_manifest_mode> = candidate`:\n\n- `--schema-contract \u003Cworking_dir>\u002F.tmp\u002Fdataverse-schema-contract.json`\n- `--approval-receipt \u003Cworking_dir>\u002F.tmp\u002Fmobile-plan-status.json`\n- `--execution-reconciliation \u003Cworking_dir>\u002F.tmp\u002Fdataverse-execution-reconciliation.json`\n- `--operation-manifest \u003Cworking_dir>\u002F.tmp\u002Fdataverse-operation-manifest.json`\n- `--publish-checkpoint \u003Cworking_dir>\u002F.tmp\u002Fdataverse-publish-pending.json`\n\nDo not reconstruct tables, columns, relationships, keys, payloads, tiers, or\nservice requirements from Markdown on this path. The gate-owned approval receipt binds\nthe exact structured contract content\u002Fhash, final plan hash, and final\nscreen\u002Fservice dependency list; `native-app-plan.md` remains the human review\nartifact.\n\nAn entirely absent fast-path handoff means\n`\u003Coperation_manifest_mode> = fallback` and preserves the standalone workflow\nbelow, beginning with Step 2 initialization. A partially supplied handoff, or\na supplied manifest\u002Fcontract\u002Freconciliation\u002Fcheckpoint that is malformed, stale,\nincomplete, or bound to different context\u002Ffiles, must fail closed: print the\nexact validation errors and return control to the orchestrator. Never jump to\nStep 4 without Step 2 initialization, partially trust a candidate, or mix its\noperations with agent-derived operations.\n\nFor an approved implementation candidate, continue to Step 3 and the existing\nStep 3c manifest validation; skip Steps 2b and 2.5–2.7. Supplied execution\nartifacts never override `--plan-only` or a planning-phase caller: those calls\nreturn the proposal without entering Step 3.\n\n#### Step 2b — Approved scoped planning context\n\nEnter only for a shared compact-planning handoff, not an approved legacy child\nor a creation fast-path candidate. The data owner passes the\nabsolute `planning_snapshot`, `architect_evidence`, and `schema_contract` paths\nplus accepted operations, `contract_sha256`, and `plan_sha256` in\n`approved_scope`. Require all fields and real implementation approval;\npartial, missing, changed, or mismatched context returns `NEEDS_CONTEXT` to\nthe owner, never a fallback to whole-plan replay or fresh approval inference.\n\nBefore Step 3, compare SHA-256 of the contract and final plan file bytes with\nthose frozen approval hashes, verify the selected target identity, and run the\nshared compact-evidence and decision validators against the supplied artifacts.\nDo not overwrite either hash to accept changed files. Read mutation intent\nfrom the normalized scoped contract, not historical Markdown rows; retain\nunaffected generated services. A proposal-only caller returns without mutation.\n\nAfter success, keep `\u003Coperation_manifest_mode> = fallback` for the existing\nstandalone execution\u002Freconciliation path, skip Steps 2.5–2.7, and proceed to\nStep 3. Planning evidence is not fresh write evidence: Step 4 still reconciles\nonly this accepted delta and its required dependencies against the live target.\nDo not fabricate a `create-mobile-app` receipt or partially supply Step 2a flags.\n\n### Step 2.5 — Path A: Parse user-provided diagram\n\nUsed when the user has an existing diagram from another tool (Visio, dbdiagram.io, screenshot, hand-drawn).\n\nAccept three input formats:\n\n| Format | How |\n|---|---|\n| **Image path** (`*.png` \u002F `*.jpg` \u002F `*.jpeg`) | Use `Read` on the file path. The vision-capable model extracts entities, columns, relationships. |\n| **Mermaid syntax** | User pastes a `erDiagram` block in chat. Parse the entities, columns, and `\\|\\|--o{` cardinalities directly. |\n| **Text description** | Parse the requested entities\u002Frelationships as intent, then use the shared snapshot-only architect handoff with the text and scoped compact evidence. |\n\nWhichever format, normalize into the same structure used by the planner agent:\n\n```yaml\npublisherPrefix: \u003Cfrom detected publisher prefix or user>\ntables:\n  - logicalName: contoso_servicevisit\n    displayName: Service Visit\n    status: new   # new | extend | reuse\n    columns: [...]\n    relationships: [...]\n```\n\nThen follow the shared planning workflow to generate `_dm_section.md` with\nthe Mermaid diagram and a normalized `.tmp\u002Fdataverse-schema-contract.json`\nfrom compact evidence. Run its decision validation for the diagram just as for\nan architect result, then continue to Step 2.7. Do not save the live plan here.\n\n### Step 2.6 — Path B: Spawn architect agent\n\nIf the user picked Path B, use the shared workflow's\n`mobile-app:data-model-architect` handoff with `Dataverse planning mode: required`,\nthe absolute snapshot\u002Fcompact-evidence paths, the scoped request, and planning\u002F\nproposal-only context. Require `_dm_section.md` and the normalized contract.\nIf the host cannot spawn agents, produce both inline from that same evidence.\nUse the shared structured-signal recovery before generic retries. A diagram\nparse failure requires resolving the intended entities, not silently inventing\na replacement model.\n\nIf they need new tables and refuse both paths, recommend `\u002Fsetup-datamodel` for\ndata-only planning and stop. Do not start another workflow without a new request.\n\n### Step 2.7 — Validate and approve the standalone delta\n\nFor every new standalone proposal, require exit `0` from\n`validate-dataverse-planning-decisions.js` for the current normalized contract\nand snapshot before presenting the existing approval gate. This includes\ndiagrams, architect results, inline fallbacks, and edits to an existing plan.\nIf either `--plan-only` or a planning-phase caller applies, return the validated\nproposal and STOP before saving the live plan or granting execution permission.\n\nOtherwise show the exact delta and its concerns with `EnterPlanMode`. Approval\nsaves only that delta into `native-app-plan.md`, preserving unaffected sections.\nFreeze the shared planning paths, accepted operations, and current\n`contract_sha256` \u002F `plan_sha256` as this invocation's approved context; verify\nit using Step 2b before implementation. Revision repeats planning validation;\ncancellation stops without saving or mutation. Already-approved scoped child\ncalls and valid creation fast-path calls do not repeat this gate.\n\n### Step 3 — Setup Dataverse Web API auth\n\nRequired only after implementation approval. For approved existing-table\nbindings with no schema mutation, skip to Step 6, not the schema-write phases.\n\n#### Step 3a — Environment consistency check\n\nThe Power Apps CLI (`$PA`) and `az` authenticate independently — they can point to different accounts. Verify `power.config.json` resolves and `az` can token for the target tenant before making any Dataverse API calls:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nENVIRONMENT_ID=$(node -p \"require('.\u002Fpower.config.json').environmentId\") || exit 1\nENV_JSON=$(node \"${PLUGIN_ROOT}\u002Fscripts\u002Fresolve-environment.js\" \"$ENVIRONMENT_ID\") || exit 1\necho \"$ENV_JSON\"\naz account show --query \"{user: user.name, tenant: tenantId}\" -o json\n```\n\nCompare the resolved environment URL with `\u003CenvUrl>` captured in Step 1. If they differ, **STOP** and warn:\n\n> \"⚠️ Environment mismatch detected:\n> - resolver reports: `\u003Cresolved_env_url>`\n> - This project targets: `\u003CenvUrl>`\n>\n> The Dataverse API token comes from `az`, which must target the same tenant as the selected environment. Run:\n> ```bash\n> cd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\n> az login --tenant \u003Ctenant-id>      # switch az to the right tenant\n> ```\n> Then re-run `\u002Fadd-dataverse`.\"\n\n**Do NOT proceed with table creation if environments don't match** — you'll create tables in the wrong org.\n\n#### Step 3b — Acquire token\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\naz account show --query \"user.name\" -o tsv\n```\n\nIf empty, instruct `az login` and stop.\n\n**Script invocation contract — read this once, all subsequent calls in this skill follow it:**\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> \u003CMETHOD> \u003CapiPath> \\\n  [--body '\u003Cjson>'] [--include-headers] \\\n  --tenant-id '\u003CtenantId-from-resolve-environment>'\n```\n\n- Three positional args, in order: `\u003CenvUrl>`, `\u003CMETHOD>` (GET \u002F POST \u002F PATCH \u002F DELETE), `\u003CapiPath>` (everything after `\u002Fapi\u002Fdata\u002Fv9.2\u002F`).\n- **Body is a flag, not positional.** `--body '\u003Cjson>'` — required for POST\u002FPATCH, never for GET\u002FDELETE. Forgetting `--body` and passing the JSON as a 4th positional arg returns a usage error.\n- `--include-headers` adds response headers (needed for `OData-EntityId` after a record create).\n- Output is JSON: `{ \"status\": \u003Ccode>, \"data\": \u003Cbody> }`. Token refresh on 401 and back-off on 429 are automatic — never wrap with manual retry.\n\n**Pass the resolved tenant explicitly (HARD — saves discovery and survives fresh shells).** `resolve-environment.js` already returned `tenantId` in Step 1. Substitute that literal value into every `--tenant-id` argument; do not rely on an exported or shell-local variable because separate tool executions may use fresh shells.\n\nIf the tenant is unknown, omit `--tenant-id` — discovery still works, it is just slower.\n\nAcquire a Dataverse access token and verify connectivity:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> GET WhoAmI \\\n  --tenant-id '\u003CtenantId-from-resolve-environment>'\n```\n\nThe explicit tenant is also reused for token refresh after a 401 and takes\npriority over shell environment variables and Azure account discovery.\n\n`WhoAmI` is the Dataverse identity endpoint — capital W\u002FA\u002FI (case-sensitive). The response gives `UserId`, `BusinessUnitId`, `OrganizationId` but **does NOT include the publisher prefix**. To get the publisher prefix, query the solution's publisher (defaults to `Default`; pass a different solution name if the env uses a custom solution):\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fdetect-publisher-prefix.js\" \u003CenvUrl> [solutionName] \\\n  --tenant-id '\u003CtenantId-from-resolve-environment>'\n# solutionName defaults to \"Default\" if omitted\n```\n\nThis runs the OData query:\n`\u002Fapi\u002Fdata\u002Fv9.2\u002Fsolutions?$select=uniquename&$expand=publisherid($select=customizationprefix)&$filter=uniquename eq '\u003CsolutionName>'`\n\nCapture `customizationprefix` from the solution's publisher (typical value: `cr123` → schema names like `cr123_jobsite`). Also capture the solution `uniquename` — needed for the `--solution` flag on every Step 5 \u002F 5b POST so artifacts land in our solution rather than landing wherever Dataverse defaults. Write both to `memory-bank.md` Power Platform context block.\n\nRequires the user to hold **System Administrator** or **System Customizer** in this environment.\n\nWhen `\u003Coperation_manifest_mode> = candidate`, validate the manifest now against\nthe resolved environment, tenant (when available), publisher, solution, current\nplan bytes, structured-schema bytes, and fresh reconciliation bytes:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fbuild-dataverse-operation-manifest.js\" \\\n  --validate '\u003Coperation-manifest-path>' \\\n  --contract '\u003Cschema-contract-path>' \\\n  --approval-receipt '\u003Capproval-receipt-path>' \\\n  --reconciliation '\u003Cexecution-reconciliation-path>' \\\n  --plan '\u003Cworking_dir>\u002Fnative-app-plan.md' \\\n  --environment-id \"\u003CenvironmentId>\" \\\n  --env-url \"\u003CenvUrl>\" \\\n  --tenant-id \"\u003CtenantId>\" \\\n  --publisher-prefix \"\u003Ccustomizationprefix>\" \\\n  --solution \"\u003Csolution-uniquename>\" \\\n  --publish-checkpoint '\u003Cpublish-checkpoint-path>' \\\n  --require-executable\n```\n\nValidation deterministically rebuilds the expected manifest from the bound\nstructured schema, fresh reconciliation, plan, context, and pending-publish checkpoint, then\ncompares the complete decisions, services, aliases, phases, API paths, and\nbodies. If validation fails, print every reported mismatch and fail closed to\nthe orchestrator. Do not execute or salvage individual operations and do not\nswitch a supplied candidate to the standalone fallback.\n\nValidate with `--require-executable`. Step 8 already performed the one fresh\nbounded reconciliation for every approved exact table and all of its\ncolumns\u002Frelationships\u002Fkeys, including the child\u002Fparent\u002FM:N relationship\ncapability managed properties. Missing capability evidence fails closed. If\nthe manifest remains non-executable, report its\nverification conflicts to the orchestrator. Do not add another read loop,\nchange an approved decision, or enter fallback mode. A non-executable\ncandidate authorizes no metadata write.\n\nIf validation with `--require-executable` succeeds, set\n`\u003Coperation_manifest_mode> = valid` and continue directly to Step 5's manifest\nexecution branch. This is the fast-v2 path: it skips the repeated\nagent-driven full reconciliation, not any safety check.\n\n### Step 4 — Reconcile every planned table and column against the target\n\n**Telemetry checkpoint: `reconcile_dataverse_schema`**\n\nIf `\u003Coperation_manifest_mode> = valid`, print:\n\n> `✓ Approved operation manifest validated — complete fresh reconciliation and derived metadata coverage are bound to this environment.`\n\nUse its `decisions` as the reconciliation matrix and skip the remainder of\nStep 4\u002F4a. Continue to Step 5. A valid manifest has no `unverified` items; its\nexplicit `reuse`, `adapt`, and `defer` rows remain visible in the final\nsummary.\n\n**Print before starting:**\n> \"→ Reconciling every planned table and column against live target metadata before any write…\"\n\nDo not use the custom-table list as the source of truth, and do not issue one request per table. Fetch **every** plan entry (`Reuse`, `Extend`, or `Create`) — including standard and managed dependencies — in a **single** filtered query that also expands their columns:\n\nOn the scoped planning path, \"every plan entry\" means only the approved\nnormalized contract and its required dependencies, never all saved-plan rows.\nDo not substitute the planning snapshot or inventory cache for this live read.\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> GET \\\n  \"EntityDefinitions?\\$select=MetadataId,LogicalName,SchemaName,IsCustomEntity,IsManaged,IsCustomizable,CanCreateAttributes,PrimaryIdAttribute,PrimaryNameAttribute&\\$filter=LogicalName eq '\u003Ctable1>' or LogicalName eq '\u003Ctable2>'&\\$expand=Attributes(\\$select=MetadataId,LogicalName,AttributeType,AttributeTypeName,RequiredLevel,IsManaged,IsCustomizable,IsPrimaryId,IsPrimaryName,SourceType,SourceTypeMask)\" \\\n  --tenant-id '\u003CtenantId-from-resolve-environment>'\n```\n\nBuild the `$filter` by OR-ing every planned logical name. This is the [documented way to query multiple table definitions at once](https:\u002F\u002Flearn.microsoft.com\u002Fpower-apps\u002Fdeveloper\u002Fdata-platform\u002Fquery-schema-definitions#basic-retrievemetadatachanges-example), and it replaces 2N requests (one entity GET plus one attributes GET per table) with one. Keep the `$expand` `$select` list to base `AttributeMetadata` properties only — a single query [cannot cast to a derived column type](https:\u002F\u002Flearn.microsoft.com\u002Fpower-apps\u002Fdeveloper\u002Fdata-platform\u002Fquery-schema-definitions#evaluate-other-options-to-retrieve-schema-definitions), so fetch `OptionSet` details separately for the rare column that needs them.\n\nRead the results as follows:\n\n- **A planned name present in `value[]`** — the table exists. Cache its expanded `Attributes` as that table's **attribute snapshot** for Steps 5a and 5b.\n- **A planned name absent from `value[]`** — the table does not exist. This is the equivalent of a 404 in the matrix below.\n- Interpret `IsCustomizable` and `CanCreateAttributes` as managed properties and read their `.Value` fields.\n\nIf the batched query itself fails (non-2xx), retry it once; if it fails again, split it into per-table queries so one unreadable name cannot hide the rest. Any name still unreadable after that is `unverified`: STOP before writes for that reconciliation scope. Authentication, permission, timeout, and malformed-response failures are not evidence that a name is free. If the URL would exceed a practical length with very many tables, split it into a few filtered queries — still far fewer than one request per table.\n\n**Only if the plan contains alternate keys or M:N relationships**, add the matching expands so Steps 5b and 5d never need their own per-item probes. `EntityDefinitions` also supports expanding [`Keys`, `ManyToManyRelationships`, `ManyToOneRelationships`, and `OneToManyRelationships`](https:\u002F\u002Flearn.microsoft.com\u002Fpower-apps\u002Fdeveloper\u002Fdata-platform\u002Fquery-schema-definitions#evaluate-other-options-to-retrieve-schema-definitions):\n\n```text\n&$expand=Attributes($select=...),Keys($select=SchemaName,KeyAttributes,EntityKeyIndexStatus),ManyToManyRelationships($select=SchemaName)\n```\n\nDo not add these expands when the plan has no keys or M:N relationships — they enlarge the response for no benefit, and standard tables carry many of both.\n\n#### Step 4a — Targeted derived-metadata barrier\n\nThe base attribute snapshot is sufficient for ordinary columns, but it cannot\nprove that a same-named lookup, choice, Boolean, or computed column has the same\nsemantics. Before classifying any such existing column as compatible:\n\n1. Write the planned derived-column contract to\n   `\u003Cworking_dir>\u002F.tmp\u002Fderived-metadata-expected.json`. Each row contains:\n   `table`, `logicalName`, `kind`, `type`, `sourceType`, plus:\n   - `lookupTarget` for lookups;\n   - exact integer\u002Flabel `options` for Choice, MultiSelect Choice, and Boolean;\n   - exact `sourceTypeMask` and serialized `formulaDefinition` for an explicitly\n     approved, maker-created computed dependency.\n2. Build one `BATCH-METADATA` GET operation list for the affected existing\n   tables only. Reuse one process\u002Ftoken and query:\n   - `ManyToOneRelationships` once per child table containing planned lookups;\n   - the applicable derived attribute collections\n     (`PicklistAttributeMetadata`, `MultiSelectPicklistAttributeMetadata`,\n     `BooleanAttributeMetadata`) once per table\u002Ftype, expanding `OptionSet`;\n   - the applicable typed attribute collection once per table\u002Ftype for any\n     explicitly reused computed column, selecting\n     `LogicalName,SourceType,SourceTypeMask,FormulaDefinition`.\n\n   Do not issue one process per column and do not scan every customizable table.\n   The exact planned names from Step 4 are the scope.\n   Write the operation array to\n   `\u003Cworking_dir>\u002F.tmp\u002Fderived-metadata-operations.json`, then run:\n\n   ```bash\n   cd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\n   node \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> \\\n     BATCH-METADATA derived-reconciliation \\\n     --operations \"$(cat '\u003Cworking_dir>\u002F.tmp\u002Fderived-metadata-operations.json')\" \\\n     --tenant-id '\u003CtenantId-from-resolve-environment>'\n   ```\n\n   Do not pass `--continue-on-error`; the first unreadable required metadata\n   collection must stop the barrier.\n3. Any non-2xx response, missing result slot, malformed option metadata, absent\n   lookup target, or unavailable `FormulaDefinition` makes that scope\n   `unverified`. **STOP before writes.** Authentication, throttling, permission,\n   and parse failures are never compatibility evidence.\n4. Normalize the live results into\n   `\u003Cworking_dir>\u002F.tmp\u002Fderived-metadata-live.json`. Each row uses:\n   `table`, `logicalName`, `type`, `sourceType`, `sourceTypeMask`,\n   `lookupTargets`, `options: [{ value, label }]`, and `formulaDefinition`.\n   Lookup target arrays must contain exactly the approved target. Choice\n   mappings must be non-empty with unique integer values and non-empty labels;\n   Boolean mappings must contain exactly values 0 and 1. Then run:\n\n   ```bash\n   cd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\n   node \"${PLUGIN_ROOT}\u002Fscripts\u002Fvalidate-derived-metadata.js\" \\\n     --expected '\u003Cworking_dir>\u002F.tmp\u002Fderived-metadata-expected.json' \\\n     --actual '\u003Cworking_dir>\u002F.tmp\u002Fderived-metadata-live.json'\n   ```\n\n5. A lookup is compatible only when its complete target set matches. Planned choice\n   values must exist with the same labels; extra live values are allowed.\n   Ordinary planned columns require `SourceType` 0. A maker-created computed\n   dependency is reusable only when its source type, source-type mask, and exact\n   `FormulaDefinition` match the approved artifact; the `Invalid` mask bit\n   always blocks reuse.\n\nThis phase is read-only and uses the V2 long-lived executor. It must not add\nper-column child-process\u002Ftoken overhead back into the fast path.\n\nBuild and print a reconciliation matrix before Step 5:\n\n| Target result | Table decision | Column decisions | Action |\n|---|---|---|---|\n| Present; all planned base and derived metadata compatible | `reuse` | existing columns `reuse` | No schema write. |\n| Present; custom columns missing; table customizable and can create attributes | `extend` | compatible `reuse`; absent custom `create` | Queue missing ordinary columns for sequential creation; relationships remain Pass 2. |\n| Absent; plan says Create; logical name uses the verified publisher prefix | `create` | ordinary columns `create` inline; lookups deferred | Create once after the complete-payload self-check. |\n| Absent; plan says Reuse\u002FExtend or dependency is standard\u002Fmanaged\u002Frequired-existing | `defer` | dependent columns `defer` | Never recreate a standard or managed table. Drop the dependent lookups\u002Fcolumns from this run, continue with everything else, and list them under Deferred in Step 9. |\n| Present; same-name column has incompatible `AttributeType` \u002F `AttributeTypeName.Value` | `extend` | incompatible column `adapt` | Auto-rename the planned column via the probe sequence below, record it in the alias map, and create it alongside the existing one. Never modify or delete the existing column. |\n| Present; columns missing but `IsCustomizable.Value=false` or `CanCreateAttributes.Value=false` | `reuse` | missing columns `defer` | The target cannot be extended by this workflow. Reuse the columns that do exist, drop the rest from this run, and list them under Deferred in Step 9. |\n| Batched query failed (non-2xx) after retry and per-table split | `unverified` | unknown | STOP before writes for the affected reconciliation scope and surface the concrete environment\u002Fauth\u002Fpermission error. |\n\n`replace` is not an automatic state in this workflow. Replacing a table or column requires an explicitly approved migration with dependency analysis and data movement, so a conflict resolves to `adapt` (rename beside it) or `defer` (leave it out) instead — both of which leave existing data untouched.\n\n**Decide-before-write barrier (HARD):** finish reconciliation for every table and column before the first metadata write. Every item must come out of Step 4 as `reuse`, `extend`, `create`, `adapt`, or `defer` — never as an unresolved conflict. Deciding renames up front is what keeps relationships, screens, and sample data pointing at the same names.\n\n**No dead ends (HARD):** a data-modelling conflict must never stop the run. Adapt it (rename beside the existing object) or defer it (drop it from this run), then keep going and report it in Step 9. Only environment faults stop this skill — failed auth, an environment mismatch, or a target the user has no privilege to write to. Those are not data-modelling problems and the user cannot resolve them by editing the plan.\n\n**Idempotency criterion (HARD):** re-running this skill against an already-applied plan MUST perform **zero** metadata writes. Every table, column, relationship, key, and calc column resolves to `reuse` or an \"already exists, skipped\" outcome from the Step 4 snapshot. If a re-run issues any POST, the reconciliation missed something — report it rather than writing. Use this as the acceptance check after any change to Steps 4, 5, or 5a–5d.\n\n### Step 5 — Create \u002F extend tables\n\n**Telemetry checkpoint: `apply_dataverse_schema_changes`**\n\n#### Valid operation-manifest execution branch\n\nWhen `\u003Coperation_manifest_mode> = valid`, do not have an agent rebuild request\nbodies. Read `execution.phases` in this fixed order:\n\n1. `tableCreates` — dependency-tier table creates with all ordinary columns\n   inline;\n2. `extensions` — missing ordinary columns on existing tables;\n3. `relationships` — lookups\u002F1:N and M:N after both endpoints exist;\n4. `alternateKeys` — after target tables and columns exist;\n5. `publish` — one `PublishXml` operation only when earlier phases contain\n   writes or a bound publish-pending checkpoint requires retry.\n\nFor each non-empty phase, write just that phase's `operations` array to\n`\u003Cworking_dir>\u002F.tmp\u002Fdataverse-operation-phase-\u003Cname>.json`. Read\n`integritySha256` and `binding.reconciliationSha256` from the validated\nmanifest, then execute every phase with the same project-local atomic journal:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nEXECUTION_JOURNAL='\u003Cworking_dir>\u002F.tmp\u002Fdataverse-metadata-execution-journal.json'\nALL_MANIFEST_OPERATIONS='\u003Cworking_dir>\u002F.tmp\u002Fdataverse-operation-all.json'\n# Write the flattened operations from every manifest phase to ALL_MANIFEST_OPERATIONS once.\n\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> \\\n  BATCH-METADATA \"manifest-\u003Cphase-name>\" \\\n  --operations \"$(cat '\u003Cworking_dir>\u002F.tmp\u002Fdataverse-operation-phase-\u003Cname>.json')\" \\\n  --solution \"\u003Csolution-uniquename>\" \\\n  --tenant-id \"\u003CtenantId>\" \\\n  --journal \"$EXECUTION_JOURNAL\" \\\n  --manifest-file \"$OPERATION_MANIFEST\" \\\n  --manifest-hash \"\u003Cmanifest.integritySha256>\" \\\n  --reconciliation-hash \"\u003Cmanifest.binding.reconciliationSha256>\" \\\n  --manifest-operations \"$(cat \"$ALL_MANIFEST_OPERATIONS\")\"\n```\n\nWait for the entire phase to finish before starting the next. This executor is\nnot OData `$batch`; it sends one request at a time, reuses one token, preserves\nmanifest order, and stops on the first non-2xx response. Never pass\n`--continue-on-error`, never parallelize phases or operations, and never\nexecute `service.requiredTables` through BATCH-METADATA.\nThe runner verifies the manifest file's integrity hash, requires the supplied\noperation array to equal one complete manifest phase byte-for-byte, and rejects\nthat phase until every operation in preceding phases is journal-complete.\n\nThe runner atomically writes `inFlight` before each request and records each\nsuccessful operation before moving to the next. Completed fingerprints are\nbased on stable request identity, not the positional index; the complete\nmanifest's unique zero-based order is validated separately. Completed\nfingerprints are skipped on an exact resume. If a process may have exited after Dataverse\naccepted a request but before the journal completion write, the runner fails\nwith `UNCERTAIN_METADATA_OPERATION`; do not replay the old operation array.\nTransport loss during a metadata mutation is immediately uncertain and is\nnever retried in-process; reads may retain transport retry behavior.\nPerform a new bounded exact reconciliation, rebuild and revalidate the full\nmanifest, then resume with both new hashes. The runner mechanically treats an\nuncertain operation omitted by the new manifest as already applied\u002Fsuperseded,\nor retries it only when the fresh reconciliation proves it is still required.\n\nIf `summary.metadataOperationCount` is zero, issue **zero metadata POSTs** and\nprint `↻ Dataverse schema already fully applied — zero metadata writes.` This\nis the required idempotent rerun behavior after successful publish. A manifest\nwith zero schema operations but one checkpoint-driven `PublishXml` operation\nmust run that publish retry; it is not a zero-write completion.\n\nThe manifest builder writes\n`\u003Cworking_dir>\u002F.tmp\u002Fdataverse-publish-pending.json` before any schema POST when\npublish will be required. Leave this checkpoint in place after any schema or\npublish failure. Delete it only after the validated `publish` phase returns\nsuccess. The next build validates its environment\u002Fsolution\u002Fplan\u002Fcontract\nbinding and integrity, merges its table list into the publish phase, and\ntherefore retries `PublishXml` even when every schema create is now\nidempotently skipped.\n\nOn a hidden POST-time name collision, stop at the failed operation and discard\nall not-yet-run phase arrays. The execution script must not choose `Adapt` or a\nrename. Return to the existing planning revision path so the structured schema\nartifact carries the approved `Adapt` names. Then perform a fresh bounded\nreconciliation and regenerate the complete aliases, downstream relationship\nand key bodies, service-required names, phases, manifest hashes, and phase\nfiles. Revalidate before resuming through the journal. Never continue an old\narray after a rename. Any non-collision failure stops the metadata path with\nits exact result.\n\nBefore overwriting the old manifest, preserve its path. After the revised plan\nand structured schema are approved through the existing flow, the top-level\nplanner\u002Forchestrator must refresh the structured service dependencies and\n`mobile-plan-status.json` receipt. This skill cannot create or restamp it.\nBind the contract through that pre-existing receipt, then roll the existing\npublish checkpoint forward:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fbuild-dataverse-operation-manifest.js\" \\\n  --roll-forward-checkpoint \"$PUBLISH_CHECKPOINT\" \\\n  --previous-manifest \"$OPERATION_MANIFEST\" \\\n  --journal \"$EXECUTION_JOURNAL\" \\\n  --contract \"$SCHEMA_CONTRACT\" \\\n  --approval-receipt \"$APPROVAL_RECEIPT\" \\\n  --plan '\u003Cworking_dir>\u002Fnative-app-plan.md' \\\n  --output \"$PUBLISH_CHECKPOINT\" \\\n  --environment-id \"\u003CenvironmentId>\" \\\n  --env-url \"\u003CenvUrl>\" \\\n  --tenant-id \"\u003CtenantId>\" \\\n  --publisher-prefix \"\u003Ccustomizationprefix>\" \\\n  --solution \"\u003Csolution-uniquename>\"\n```\n\nThis retains prior checkpoint bindings\u002Ftables as integrity-protected history,\nkeeps earlier successful tables publication-pending, and maps only the\njournal-proven failed collision table to its revised in-contract alias. It\nfails closed if a completed write would disappear from the revised contract or\nchange definition: completed tables\u002Finline columns, extension columns, image\nconfiguration PUTs, relationships (including cascade behavior), and alternate keys must each map\nto an equivalent revised structured component. Completed image PUTs retain their\nfull hash-bound definitions only when the revised image requirements remain\ncompatible; they stay publication-pending without replaying the update. It also\nrejects any unrelated out-of-contract publish target. Only after this succeeds\nmay Step 8 overwrite the operation manifest.\n\nThe manifest builder never emits calculated\u002Frollup\u002Fformula creation. Reused\ncomputed dependencies have already crossed the exact derived-metadata barrier;\nunsupported projections are explicit `defer` rows. After the `publish` phase succeeds, delete the publish checkpoint and invalidate the planning-only inventory cache:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode -e \"const checkpoint = process.argv[1]; if (checkpoint) require('node:fs').rmSync(checkpoint, { force: true });\" \\\n  \"${PUBLISH_CHECKPOINT:-}\"\nif ! node \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-inventory-cache.js\" \\\n  --file '\u003Cworking_dir>\u002F.tmp\u002Fdataverse-inventory-cache.json' --invalidate; then\n  printf 'NEEDS_RECOVERY: dataverse-inventory-cache\\n' >&2\n  exit 2\nfi\n```\n\n**Cache-invalidation recovery (both publish paths):** a nonzero exit returns\ncontrol to the foreground agent for local recovery, not success. Diagnose the\ncache path or filesystem permission error and allow at most two targeted retries\nof the invalidation command. Preserve the successful publish result and execution\njournal; do not replay metadata writes or publish to repair a local cache failure.\nDo not reuse the failed cache or continue to service generation, verification,\nsample data, or offline setup until invalidation exits `0`. If local recovery\ncannot complete, report the remaining blocker and resume this cleanup after it is\nresolved; never mark the phase complete just because publication succeeded.\n\nOnly after successful invalidation continue to Step 6. When there were zero writes and no checkpoint,\ncontinue without deleting anything. Skip the\nfallback mutation instructions in Steps 5a–5d and skip Step 6b because publish\nwas already part of the validated phase order.\n\n**Print before starting:**\n> \"→ Creating\u002Fextending \u003CN> tables in tier order (sequential — Dataverse serializes metadata writes). For each: pre-flight check, then 'Creating \u003Ctable>…' before the POST and '✓ \u003Ctable>' on 2xx response.\"\n\n> **⚠️ Concurrency rule — do not violate.** All Dataverse metadata operations in Steps 5, 6, and 6b are **strictly sequential**: issue one HTTP request, wait for a 2xx response, then issue the next. Do NOT parallelize or use OData `$batch`. Dataverse serializes metadata writes via an exclusive lock; parallel calls return `429 TooManyRequests`, `MetadataLockHeldException`, or `404 EntityNotFound` for lookups whose parent hasn't committed yet.\n>\n> Specifically:\n> - **Within a tier:** create tables one at a time.\n> - **Across tiers:** Tier 0 fully done (all tables + all columns committed) before any Tier 1 POST.\n> - **Lookups:** POST to `\u002FRelationshipDefinitions` only **after** both endpoint tables exist and have returned 2xx.\n> - **Extensions:** column POSTs to an existing table are also serial — same lock applies.\n\nFor multiple already-reconciled operations, prefer the local `BATCH-METADATA`\nexecutor. It is **not** OData `$batch`: one Node process reuses one token and\nissues requests strictly one at a time in array order, stopping on the first\nnon-2xx response by default.\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> \\\n  BATCH-METADATA schema-writes \\\n  --operations '\u003Cordered-json-array>' \\\n  --solution '\u003Csolution-uniquename-from-memory-bank>' \\\n  --tenant-id '\u003CtenantId-from-resolve-environment>'\n```\n\nEach operation is `{ \"index\", \"method\", \"apiPath\", \"body\" }`; an operation may\noverride the command-level `solution`. Build the array only after the full\nmetadata snapshot and desired\u002Flive diff. Preserve dependency order: new tables\nwith ordinary columns inline, extension columns, relationships, projections,\nthen alternate keys. Never pass `--continue-on-error` for schema creation. The\nresult includes per-operation `status` and `durationMs`; after a failure,\nreconcile that component and resume with only the remaining operations.\n\n#### Step 5a — Pre-flight collision check (from the Step 4 snapshot)\n\nBefore each create, confirm the target name is actually free: name-prefix collisions from stale solutions, reserved system names, and soft-deleted tombstones all fail the POST, and Dataverse takes ~1 minute to return the conflict error. A failure here can leave Tier 0 partially created and make a Tier 1 lookup fail on a phantom parent. Step 4 already collected this evidence for every planned name, so this step reads it rather than re-querying.\n\n**For every `Create` entry, resolve its target state from the Step 4 batch — do not re-query per table.** Step 4 already fetched every planned logical name, so reuse that result:\n\n| Step 4 result for this name | Meaning | Action |\n|---|---|---|\n| **Absent from `value[]`** | Name is free | Proceed with POST. |\n| **Present** + `IsCustomEntity: true` + `MetadataId` matches memory-bank | We created this earlier — idempotent re-run | Skip the POST, mark as created, continue. |\n| **Present** + `IsCustomEntity: true` + `MetadataId` *not* in memory-bank | Foreign collision | Reconcile live columns and customization properties below; never auto-extend an uncustomizable target. |\n| **Present** + `IsCustomEntity: false` | Reserved system table name | Auto-recover via rename (see below). |\n\nOnly re-probe a single name when Step 4's batch did not cover it (for example a rename candidate generated later in this step):\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> GET \\\n  \"EntityDefinitions(LogicalName='\u003Cprefix>_\u003Ctable>')?\\$select=MetadataId,LogicalName,IsCustomEntity,IsManaged,IsCustomizable,CanCreateAttributes\" \\\n  --tenant-id '\u003CtenantId-from-resolve-environment>'\n```\n\nTombstones and hidden collisions are **not** reliably visible to either form — Dataverse can report a name as free and still reject the POST minutes later. Those are caught by the POST-time collision rescue below, which is the real safety net:\n\n| POST response | Meaning | Action |\n|---|---|---|\n| **5xx** with `0x80060890` or message `\"object with same name exists in solution\"` | Tombstone (soft-deleted, ~30 min purge TTL) | Auto-recover via rename (see below). |\n| **400** with `0x80044363`, `\"schema name ... is not unique\"`, or `\"same name already exists\"` | Hidden Dataverse collision \u002F recent-delete tombstone | Auto-recover via rename (see below), then retry the POST once. |\n\n**Important:** treat a POST-time collision as a recoverable name conflict, not a data-model failure — the schema name can stay reserved internally after a delete even when metadata reports it as free.\n\n#### Auto-recovery — reuse\u002Fextend first, rename as last resort\n\n**Priority order when Step 5a hits a name collision:**\n\n1. **Adopt as Extend (preferred)** — only if the existing table is the same concept, every same-name column is type-compatible, planned missing columns are custom additions, and live `IsCustomizable.Value` plus `CanCreateAttributes.Value` both permit extension. Add only the missing columns via Step 5b and log `→ Extending existing \u003Coriginal> with \u003CN> missing columns.`\n2. **Adopt as Reuse** — if the existing table's schema already covers all planned columns: skip Step 5b for this entry, keep it in Step 6 for service generation. No prompt. Log `→ Reusing existing \u003Coriginal> (all required columns present).`\n3. **Rename and Create (last resort)** — only when the existing table is a fundamentally different entity (e.g., planned table is an inspection log but existing `\u003Coriginal>` is a payroll record — incompatible concept, incompatible columns). Prompt the user before proceeding.\n\n**When to auto-decide vs. prompt:**\n\n| Situation | Action |\n|---|---|\n| Foreign collision + compatible concept\u002Fschema + extension allowed | Auto-Extend (no prompt) |\n| Foreign collision + all planned columns present | Auto-Reuse (no prompt) |\n| Foreign collision + incompatible column or extension forbidden | Auto-rename the conflicting column beside it, or defer it (no prompt) |\n| Foreign collision + incompatible concept | Prompt (see below) |\n| Reserved system name | Auto-rename (no prompt) |\n| Tombstone (0x80060890 \u002F same-name-exists) | Auto-rename (no prompt) |\n\n**For the incompatible-concept case only** — prompt via `AskUserQuestion`:\n\n```\n| Option | What it means |\n|---|---|\n| Rename and Create (default) | Use a free custom logical name for the genuinely different entity. Existing table stays untouched. |\n| Reuse existing as-is | Point the generated services at the existing table and skip the planned columns it lacks. |\n```\n\nNever offer Extend for an incompatible concept or column shape. This prompt is a preference, not a gate: an empty, skipped, or unanswered response defaults to **Rename and Create** so the run always proceeds.\n\nMaintain a run-level logical-name alias map for every auto-rename. Example:\n\n```json\n{ \"cr3e9_aircraft\": \"cr3e9_aircraftv2\" }\n```\n\nBefore building any later table, column, lookup relationship, sample-data payload, service-reference text, or screen data spec, resolve logical names through this map. A rename that only changes the table POST but leaves relationships\u002Fscreens\u002Fsample data pointing at the old name is a bug.\n\n**Auto-rename probe sequence** (cap at 4 probes — only used for reserved\u002Ftombstone cases):\n\n```\n\u003Coriginal>v2  →  \u003Coriginal>v3  →  \u003Coriginal>2  →  \u003Coriginal>copy\n```\n\nFor each candidate in order, GET `EntityDefinitions(LogicalName='\u003Ccandidate>')?$select=MetadataId,IsCustomEntity`:\n- 404 → free, **take it**, stop probing.\n- 200 or 5xx (collision) → next candidate.\n\nIf all 4 collide, keep probing `\u003Coriginal>3`, `\u003Coriginal>4`, … through `\u003Coriginal>20`. This sequence is designed never to dead-end: if even those collide, use `\u003Coriginal>\u003C4-char run token>`, which is unique to this run. Never abandon a table for want of a free name.\n\n**On a successful auto-rename, do these in order BEFORE the POST:**\n\n1. **Update `native-app-plan.md`** — `Edit` with `replace_all: true` to swap the old logical name for the new one across the entire `## Data Model` section (Mermaid ER, Reuse\u002FExtend\u002FCreate table, Creation Order, Notes). This catches downstream relationship POSTs in this same Step 5 too.\n2. **Update `## Screens` per-screen specs** — same `replace_all` sweep for any service \u002F data-source references using the old name.\n3. **Append to `memory-bank.md` Collision history** — `\u003Coriginal> → \u003Cnew>` with reason (foreign \u002F reserved \u002F tombstone) and timestamp.\n4. **Update the run-level alias map** — every later metadata payload and plan edit resolves `\u003Coriginal>` to `\u003Cnew>` before use.\n5. **Inform the user — single line, no prompt:**\n   > `→ Collision on \u003Coriginal> (\u003Cforeign|reserved|tombstone>). Renamed to \u003Cnew> and updated plan + memory-bank. Continuing.`\n\nThen proceed with the POST using `\u003Cnew>`.\n\n#### Post-create collision rescue — hidden tombstone \u002F recent delete\n\nIf the Step 5b table POST fails after a 404 preflight with any Dataverse name-collision signature, **do not fail the run**:\n\n- HTTP `400` with code `0x80044363`\n- message contains `schema name` and `not unique`\n- message contains `same name already exists`\n- message contains `object with same name exists in solution`\n\nFirst attempt auto-Extend: compare the plan against that table's attribute snapshot from Step 4 (re-fetch only if it is absent). If ≥50% overlap, switch to Extend path (add missing columns). Otherwise, run the auto-rename probe sequence, update `native-app-plan.md`, `## Screens`, `memory-bank.md`, and the run-level alias map, then retry the table POST exactly once with the resolved name. Print:\n\n> `→ Dataverse still has \u003Coriginal> reserved from a recent delete\u002Fhidden collision. Using \u003Cnew> and continuing.`\n\nIf the retry also returns a collision signature, continue probing the remaining candidates, then the numeric tail, then the run-token form described above.\n\n**On successful POST**, immediately re-GET to capture the server-assigned `MetadataId` and write it to memory-bank (Step 6d updates `.datamodel-manifest.json`; you also append to `memory-bank.md` under \"Created tables\" with the GUID and solution name). This lets future `\u002Fadd-dataverse` runs distinguish \"we own this\" from \"name collision.\"\n\n#### Step 5b — Create \u002F extend\n\nRun this step in two explicit passes:\n\n1. **Pass 1 — tables + ordinary columns:** create every new table with all planned non-lookup columns inline, then extend existing tables with any missing non-lookup columns. Computed formula definitions are never synthesized by this workflow.\n2. **Pass 2 — lookups + relationships:** only after Pass 1 has succeeded for every tier, create the planned `RelationshipDefinitions`. A lookup is created by its relationship and must not appear in a table's initial `Attributes` array.\n\nBoth passes remain strictly sequential. Do not use `$batch` for metadata writes.\n\nFor each `Create` decision, in **tier order** (Tier 0 → Tier 1 → Tier 2 → …), POST a new EntityDefinition. Skip if Step 5a returned a known-self match (idempotent).\n\n> **⚠️ Inline ALL planned columns into the Create POST body — do NOT POST columns individually.**\n>\n> Dataverse accepts all non-lookup columns in the initial table's `Attributes: [...]` array. Inline form: 1 round trip, ~3-8s. Per-column form: N+1 round trips, each ~3-8s. For a 5-column table that's roughly 24s saved on the lock-serialized metadata path. Do not describe this metadata create as transactionally atomic: if the request fails after Dataverse starts processing it, the table or some attributes may remain, which is why the recovery path below exists.\n>\n> **Wrong** (N round trips):\n> ```json\n> { \"SchemaName\": \"...\", \"Attributes\": [{ \u002F* primary only *\u002F }] }\n> \u002F\u002F then 4× POST \u002FAttributes for the rest\n> ```\n>\n> **Right** (1 round trip):\n> ```json\n> {\n>   \"SchemaName\": \"...\",\n>   \"Attributes\": [\n>     { \u002F* primary name *\u002F },\n>     { \u002F* column 2 *\u002F },\n>     { \u002F* column 3 *\u002F },\n>     { \u002F* column 4 *\u002F },\n>     { \u002F* column 5 *\u002F }\n>   ]\n> }\n> ```\n>\n> The per-column POST path remains valid for two cases only: (1) Extend on an existing table, (2) retry-after-partial-failure when Step 5a's pre-flight shows the table exists but some columns don't.\n\n**Solution targeting (HARD):** every Step 5 \u002F 5b POST MUST pass `--solution \u003Cuniquename>` so Dataverse routes the new artifact into our solution rather than the unmanaged default. Read the solution name from `memory-bank.md` Power Platform context (captured in Step 3b). Without this flag, multi-project environments end up with cross-solution leakage and the foreign-collision class of bug returns. The script translates `--solution` to the `MSCRM.SolutionUniqueName` HTTP header.\n\n**Scratch files:** When writing request body JSON to disk (e.g. table definitions, column metadata, relationship payloads), always write to `\u003Cworking_dir>\u002F.tmp\u002F`, never to `\u002Ftmp\u002F`. Keeping request bodies project-local prevents cross-project writes and makes cleanup deterministic. After the per-call root guard, create the folder if it doesn't exist: `mkdir -p .tmp`.\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> POST EntityDefinitions \\\n  --body '\u003Cjson-body-with-all-columns-inline>' \\\n  --solution '\u003Csolution-uniquename-from-memory-bank>' \\\n  --tenant-id '\u003CtenantId-from-resolve-environment>'\n```\n\n**Complete-payload self-check (HARD — do this before the POST, no extra tooling):** re-read the body you just built against the plan and confirm all five statements. If any fails, fix the body and re-check; never POST a partial table and repair it with per-column POSTs.\n\n1. **Every planned ordinary column is present** — String, Memo, Integer, BigInt, Decimal, Money, DateTime, Boolean, Choice, MultiSelect Choice, Image, and File. Count `Attributes[]` and compare with the plan's column count for this table.\n2. **No lookup metadata is inline** — no `LookupAttributeMetadata`, `CustomerAttributeMetadata`, or `OwnerAttributeMetadata`. Those are created by their relationship in Pass 2.\n3. **No deferred or server-owned column is inline** — no primary-id column and no calculated\u002Frollup column (Step 5c owns those).\n4. **Exactly one `IsPrimaryName: true` attribute exists**, and `PrimaryNameAttribute` matches its `SchemaName` (lowercased logical form).\n5. **No duplicate `SchemaName`** in `Attributes[]` (compare case-insensitively).\n\nMicrosoft documents both halves of this contract: [ordinary columns may be included when the table is created](https:\u002F\u002Flearn.microsoft.com\u002Fpower-apps\u002Fdeveloper\u002Fdata-platform\u002Fwebapi\u002Fcreate-update-column-definitions-using-web-api#create-columns), while a [lookup is created with its one-to-many relationship](https:\u002F\u002Flearn.microsoft.com\u002Fpower-apps\u002Fdeveloper\u002Fdata-platform\u002Fwebapi\u002Fcreate-update-entity-relationships-using-web-api#create-a-one-to-many-relationship). `dataverse-request.js` stays the only script in this path — it already owns auth, retry, `--solution` routing, and 401\u002F429 handling.\n\nBody skeleton — **all planned columns inline in `Attributes: [...]`** (this example shows primary + 3 additional; expand the array to fit every column from the plan):\n\n> **⚠️ `IsAvailableOffline` + `ChangeTrackingEnabled` MUST be set to `true` at create time** for any table the app intends to make available offline. Without these two flags the table cannot be added to a `mobileofflineprofile`, and `\u002Fsetup-offline-profile` will have to fix them via a separate metadata PUT (the `\u002Fenable-tables-offline` skill handles that, but it doubles the metadata-lock-serialized round trips). Empirically verified 2026-05-18 in the chanel-rm demo: 7 custom tables created without these flags caused 7 prereq-revert drift entries; fixed by post-hoc enablement. Default these to `true` for all UserOwned tables created by `\u002Fadd-dataverse` unless the user has explicitly opted out of offline support. The flags are no-ops at runtime for apps that don't use offline profiles.\n\n```json\n{\n  \"@odata.type\": \"Microsoft.Dynamics.CRM.EntityMetadata\",\n  \"SchemaName\": \"cr123_jobsite\",\n  \"DisplayName\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"Label\": \"Job Site\", \"LanguageCode\": 1033 }] },\n  \"DisplayCollectionName\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"Label\": \"Job Sites\", \"LanguageCode\": 1033 }] },\n  \"OwnershipType\": \"UserOwned\",\n  \"HasActivities\": false,\n  \"HasNotes\": false,\n  \"IsAvailableOffline\": true,\n  \"ChangeTrackingEnabled\": true,\n  \"PrimaryNameAttribute\": \"cr123_sitename\",\n  \"Attributes\": [\n    {\n      \"@odata.type\": \"Microsoft.Dynamics.CRM.StringAttributeMetadata\",\n      \"SchemaName\": \"cr123_sitename\",\n      \"DisplayName\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"Label\": \"Site Name\", \"LanguageCode\": 1033 }] },\n      \"MaxLength\": 200,\n      \"FormatName\": { \"Value\": \"Text\" },\n      \"RequiredLevel\": { \"Value\": \"ApplicationRequired\" },\n      \"IsPrimaryName\": true\n    },\n    {\n      \"@odata.type\": \"Microsoft.Dynamics.CRM.StringAttributeMetadata\",\n      \"SchemaName\": \"cr123_address\",\n      \"DisplayName\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"Label\": \"Address\", \"LanguageCode\": 1033 }] },\n      \"MaxLength\": 500,\n      \"FormatName\": { \"Value\": \"Text\" },\n      \"RequiredLevel\": { \"Value\": \"None\" }\n    },\n    {\n      \"@odata.type\": \"Microsoft.Dynamics.CRM.IntegerAttributeMetadata\",\n      \"SchemaName\": \"cr123_squarefeet\",\n      \"DisplayName\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"Label\": \"Square Feet\", \"LanguageCode\": 1033 }] },\n      \"RequiredLevel\": { \"Value\": \"None\" },\n      \"MinValue\": 0,\n      \"MaxValue\": 2147483647,\n      \"Format\": \"None\"\n    },\n    {\n      \"@odata.type\": \"Microsoft.Dynamics.CRM.BooleanAttributeMetadata\",\n      \"SchemaName\": \"cr123_active\",\n      \"DisplayName\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"Label\": \"Active\", \"LanguageCode\": 1033 }] },\n      \"RequiredLevel\": { \"Value\": \"None\" },\n      \"DefaultValue\": true,\n      \"OptionSet\": {\n        \"@odata.type\": \"Microsoft.Dynamics.CRM.BooleanOptionSetMetadata\",\n        \"TrueOption\": { \"Value\": 1, \"Label\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"Label\": \"Yes\", \"LanguageCode\": 1033 }] } },\n        \"FalseOption\": { \"Value\": 0, \"Label\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"Label\": \"No\", \"LanguageCode\": 1033 }] } }\n      }\n    }\n  ]\n}\n```\n\nFor each `Extend` decision, POST a new column to the existing table.\n\n> **⚠️ Table-level pre-flight (HARD — required for idempotent re-runs).** Reuse the complete attribute snapshot fetched for this table in Step 4. If the table was discovered only during collision recovery, or no current snapshot exists, fetch all attributes exactly once:\n>\n> ```bash\n> cd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\n> node \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> GET \\\n>   \"EntityDefinitions(LogicalName='\u003Ctable>')\u002FAttributes?\\$select=MetadataId,LogicalName,SchemaName,AttributeType,AttributeTypeName,RequiredLevel,IsManaged,IsCustomizable,IsPrimaryId,IsPrimaryName\" \\\n>   --tenant-id '\u003CtenantId-from-resolve-environment>'\n> ```\n>\n> Build a local `{ lowerCaseLogicalName → { AttributeType, AttributeTypeName, IsManaged, IsCustomizable } }` map and classify **every** planned non-lookup column before issuing any POST. Prefer `AttributeTypeName.Value` when `AttributeType` is `Virtual`, so File and Image columns are not incorrectly treated as compatible generic virtual attributes:\n>\n> | Snapshot result | Action |\n> |---|---|\n> | Name exists and `AttributeType` matches | Skip it and log `↻ \u003Ccolumn> (already exists, skipped)`. |\n> | Name exists and `AttributeType` differs | Dataverse does not allow column-type changes via API, so **auto-rename the planned column** using the same probe sequence (`\u003Ccolumn>v2` → `\u003Ccolumn>v3` → `\u003Ccolumn>2` → …), add it to the alias map, and create it beside the existing one. Log `→ \u003Ccolumn> exists as \u003CexistingType>; created \u003CnewName> as \u003CplannedType> instead.` Never modify or delete the existing column. |\n> | Name is absent | Add it to the ordered missing-column queue. |\n>\n> This single snapshot catches partial creates, corrected re-runs, and network-drop recovery without paying one GET round trip per column.\n\nAfter the complete comparison passes, POST the missing-column queue **one column at a time, sequentially** (no `$batch`; always pass `--solution`):\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> POST \\\n  \"EntityDefinitions(LogicalName='\u003Ctable>')\u002FAttributes\" \\\n  --body '\u003Ccolumn-json>' \\\n  --solution '\u003Csolution-uniquename-from-memory-bank>' \\\n  --tenant-id '\u003CtenantId-from-resolve-environment>'\n```\n\n**Recovery after a partial Create:** do not re-POST the whole `EntityDefinitions` body because the table now exists and Dataverse returns `0x80060888`. Fetch that table's complete attribute snapshot once, run the same local comparison, and sequentially POST only the missing non-lookup columns. This is the only time a table originally classified as Create should use the per-column path.\n\nColumn shapes that have non-obvious gotchas (handle carefully):\n- **Pass-2 barrier (HARD)** — do not create any lookup or relationship while Pass 1 is still creating or extending tables. Accumulate relationship definitions, wait until every table and ordinary column has returned 2xx, then process the relationships sequentially in dependency order.\n- **Lookup** — POST to `\u002FRelationshipDefinitions`, not `\u002FAttributes`.\n\n  > **⚠️ Do NOT improvise the body. Copy the skeleton below verbatim and replace only the placeholders in `\u003C>` brackets.**\n  >\n  > Fields that cause silent failure if added:\n  > - **Do NOT include `ReferencingAttribute`.** Dataverse auto-creates the foreign-key column from `Lookup.SchemaName`. Including it causes `404: Could not find an attribute with specified name` because the column doesn't exist yet at POST time.\n  > - **Do NOT include `Lookup.LogicalName`.** It's read-only metadata; including it returns `400 Bad Request`.\n  > - **Do NOT include `ReferencedAttribute`.** Dataverse resolves the primary key of the referenced entity automatically. The reference is optional and omitting it is the correct default.\n  >\n  > Required fields: `SchemaName`, `ReferencedEntity`, `ReferencingEntity`, `Lookup.{@odata.type, SchemaName, DisplayName, RequiredLevel}`, `AssociatedMenuConfiguration`, `CascadeConfiguration` (including `RollupView`). Anything else is invented — drop it.\n\n  ```json\n  {\n    \"@odata.type\": \"Microsoft.Dynamics.CRM.OneToManyRelationshipMetadata\",\n    \"SchemaName\": \"\u003Cprefix>_\u003CParent>_\u003CChild>\",\n    \"ReferencedEntity\": \"\u003Cparent_table_logical_name>\",\n    \"ReferencingEntity\": \"\u003Cchild_table_logical_name>\",\n    \"Lookup\": {\n      \"@odata.type\": \"Microsoft.Dynamics.CRM.LookupAttributeMetadata\",\n      \"SchemaName\": \"\u003CPrefix>_\u003CParent>Id\",\n      \"DisplayName\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"@odata.type\": \"Microsoft.Dynamics.CRM.LocalizedLabel\", \"Label\": \"\u003CParent Display Name>\", \"LanguageCode\": 1033 }] },\n      \"RequiredLevel\": { \"Value\": \"None\" }\n    },\n    \"AssociatedMenuConfiguration\": { \"Behavior\": \"UseCollectionName\", \"Group\": \"Details\", \"Order\": 10000 },\n    \"CascadeConfiguration\": {\n      \"Assign\": \"NoCascade\",\n      \"Delete\": \"RemoveLink\",\n      \"Merge\": \"NoCascade\",\n      \"Reparent\": \"NoCascade\",\n      \"Share\": \"NoCascade\",\n      \"Unshare\": \"NoCascade\",\n      \"RollupView\": \"NoCascade\"\n    }\n  }\n  ```\n\n  Invocation (apiPath is `RelationshipDefinitions`, body via `--body`, always pass `--solution`):\n\n  ```bash\n  cd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\n  node \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> POST \\\n    RelationshipDefinitions \\\n    --body '\u003Cjson-body-from-skeleton-above>' \\\n    --solution '\u003Csolution-uniquename-from-memory-bank>' \\\n    --tenant-id '\u003CtenantId-from-resolve-environment>'\n  ```\n\n  **Pre-flight the lookup (HARD — required for idempotent re-runs).** A lookup can only pre-exist if the **referencing (child) table** already existed at Step 4, so when the child was created in this run, skip the probe and POST. Otherwise look for the lookup's foreign-key column — the lowercased `Lookup.SchemaName`, e.g. `\u003Cprefix>_\u003Cparent>id` — in that child table's Step 4 attribute snapshot:\n\n  | Snapshot result | Action |\n  |---|---|\n  | Present with `AttributeType: Lookup` | Skip the POST and log `↻ \u003CSchemaName> (relationship already exists, skipped)`. |\n  | Present with any other `AttributeType` | A non-lookup column already owns that name. Auto-rename the lookup's `Lookup.SchemaName` via the probe sequence, record it in the alias map, and POST the relationship with the new name. Never overwrite the existing column. |\n  | Absent | POST the relationship. |\n\n  This costs no extra round trip: the referencing attribute is an ordinary attribute on the child table, so it is already in the snapshot Step 4 fetched. Without this check a re-run POSTs a duplicate relationship and fails the run mid-Pass-2.\n\n- **Many-to-Many (M:N)** — also POST to `\u002FRelationshipDefinitions`, but with `ManyToManyRelationshipMetadata`. Dataverse creates an auto-named intersect table.\n\n  > **⚠️ Do NOT improvise the body.** Required fields: `SchemaName`, `Entity1LogicalName`, `Entity2LogicalName`, `IntersectEntityName`, and the two `AssociatedMenuConfiguration` blocks. Do not include lookup or cascade fields — those are 1:N concepts.\n\n  ```json\n  {\n    \"@odata.type\": \"Microsoft.Dynamics.CRM.ManyToManyRelationshipMetadata\",\n    \"SchemaName\": \"\u003Cprefix>_\u003Ctable1>_\u003Ctable2>\",\n    \"Entity1LogicalName\": \"\u003Ctable1_logical_name>\",\n    \"Entity2LogicalName\": \"\u003Ctable2_logical_name>\",\n    \"IntersectEntityName\": \"\u003Cprefix>_\u003Ctable1>_\u003Ctable2>\",\n    \"Entity1AssociatedMenuConfiguration\": {\n      \"Behavior\": \"UseLabel\",\n      \"Group\": \"Details\",\n      \"Label\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"@odata.type\": \"Microsoft.Dynamics.CRM.LocalizedLabel\", \"Label\": \"\u003CTable2 Plural>\", \"LanguageCode\": 1033 }] },\n      \"Order\": 10000\n    },\n    \"Entity2AssociatedMenuConfiguration\": {\n      \"Behavior\": \"UseLabel\",\n      \"Group\": \"Details\",\n      \"Label\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"@odata.type\": \"Microsoft.Dynamics.CRM.LocalizedLabel\", \"Label\": \"\u003CTable1 Plural>\", \"LanguageCode\": 1033 }] },\n      \"Order\": 10000\n    }\n  }\n  ```\n\n  **Pre-flight M:N:** a relationship can only pre-exist on a table that already existed at Step 4. If **either** endpoint was just created in this run, skip the probe — nothing can be there. Otherwise read `ManyToManyRelationships` from that table's Step 4 snapshot: present → skip (already exists); absent → proceed. Query `RelationshipDefinitions(SchemaName='\u003Cprefix>_\u003Ctable1>_\u003Ctable2>')?$select=SchemaName` only when the snapshot did not cover it.\n\n  **In the generated service:** M:N relationships are queried via the intersect entity name (e.g., `cr123_tag_inspection`) — the SDK does not expose a direct M:N navigation helper; the screen-builder must query the intersect table directly. Flag this in the Step 7 summary if any M:N relationships are created.\n\n- **Column `@odata.type` and required fields — reference table (verified against Dataverse OData API):**\n\n  | Dataverse type | `@odata.type` | Required extra fields |\n  |---|---|---|\n  | Single-line text | `Microsoft.Dynamics.CRM.StringAttributeMetadata` | `MaxLength` (200), `FormatName: { \"Value\": \"Text\" }` — values: `Text`, `Email`, `Url`, `Phone`, `TextArea` |\n  | Multi-line text | `Microsoft.Dynamics.CRM.MemoAttributeMetadata` | `MaxLength` (10000), `Format: \"TextArea\"` |\n  | Whole number | `Microsoft.Dynamics.CRM.IntegerAttributeMetadata` | `MinValue`, `MaxValue`, `Format: \"None\"` |\n  | Decimal | `Microsoft.Dynamics.CRM.DecimalAttributeMetadata` | `MinValue`, `MaxValue`, `Precision` (2) |\n  | Currency (Money) | `Microsoft.Dynamics.CRM.MoneyAttributeMetadata` | `MinValue`, `MaxValue`, `Precision` (2), `PrecisionSource` (2) |\n  | Date\u002FTime | `Microsoft.Dynamics.CRM.DateTimeAttributeMetadata` | `Format: \"DateAndTime\"` or `\"DateOnly\"`, `DateTimeBehavior: { \"Value\": \"UserLocal\" }` |\n  | Boolean | `Microsoft.Dynamics.CRM.BooleanAttributeMetadata` | `DefaultValue`, `OptionSet` with `TrueOption`\u002F`FalseOption` |\n  | Choice (picklist) | `Microsoft.Dynamics.CRM.PicklistAttributeMetadata` | `OptionSet` with `IsGlobal: false`, `OptionSetType: \"Picklist\"`, `Options[]` — **option integer values start at `100000000` and increment by 1** |\n  | Lookup | via `RelationshipDefinitions` — see 1:N skeleton above | — |\n  | Image | `Microsoft.Dynamics.CRM.ImageAttributeMetadata` | `MaxSizeInKB` (default 10240), `CanStoreFullImage` |\n  | File | `Microsoft.Dynamics.CRM.FileAttributeMetadata` | `MaxSizeInKB` |\n\n  **Common mistake:** omitting `FormatName` on String columns and `DateTimeBehavior` on DateTime columns. Both are required — Dataverse rejects the POST without them.\n\n- **Choice (option set)** — set `OptionSet.IsGlobal: false` for local picklists. Full body (option values start at `100000000` and increment by 1):\n\n  ```json\n  {\n    \"@odata.type\": \"Microsoft.Dynamics.CRM.PicklistAttributeMetadata\",\n    \"SchemaName\": \"\u003CPrefix>_\u003CColumnName>\",\n    \"DisplayName\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"@odata.type\": \"Microsoft.Dynamics.CRM.LocalizedLabel\", \"Label\": \"\u003CDisplay Name>\", \"LanguageCode\": 1033 }] },\n    \"RequiredLevel\": { \"Value\": \"None\" },\n    \"OptionSet\": {\n      \"@odata.type\": \"Microsoft.Dynamics.CRM.OptionSetMetadata\",\n      \"IsGlobal\": false,\n      \"OptionSetType\": \"Picklist\",\n      \"Options\": [\n        { \"Value\": 100000000, \"Label\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"@odata.type\": \"Microsoft.Dynamics.CRM.LocalizedLabel\", \"Label\": \"Option 1\", \"LanguageCode\": 1033 }] } },\n        { \"Value\": 100000001, \"Label\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"@odata.type\": \"Microsoft.Dynamics.CRM.LocalizedLabel\", \"Label\": \"Option 2\", \"LanguageCode\": 1033 }] } }\n      ]\n    }\n  }\n  ```\n- **Image** — `@odata.type: Microsoft.Dynamics.CRM.ImageAttributeMetadata`,\n  `MaxSizeInKB` and `CanStoreFullImage`. Dataverse always reports\n  `MaxHeight: 144` and `MaxWidth: 144` for the generated thumbnail; those values\n  cannot be changed and must not be used as full-image dimensions. Set\n  `CanStoreFullImage: true` when users must inspect or download the retained\n  full-size image. Updating this setting requires retrieving the complete\n  current `ImageAttributeMetadata`, changing the writable property, sending a\n  full-definition `PUT`, and publishing customizations.\n- **File** — `@odata.type: Microsoft.Dynamics.CRM.FileAttributeMetadata`, `MaxSizeInKB` required\n\nIf the column type is not a simple string\u002Fint\u002Fboolean, surface a one-line confirmation to the user before posting.\n\nAfter all mutations, re-run the existing-tables query (Step 4) to confirm everything landed.\n\n### Step 5c — Enforce the supported computed-column boundary\n\nDataverse metadata exposes `FormulaDefinition`, but Microsoft explicitly does\nnot support defining calculated, rollup, or formula expressions through code.\nLegacy workflow XAML and generated formula serialization must not be synthesized.\n\n- Do not invoke `create-calculated-column.js`; it is a fail-closed guard for\n  legacy callers.\n- Cross-entity fields must use a supported formatted lookup annotation or\n  bounded chained fetch as documented in `data-performance.md`.\n- For a hot list field that cannot use either path, mark it\n  `external-projection-required` and omit it from this mutation run. The user\n  may create a formula column in Power Apps or supply another server-owned\n  projection outside this PR, then rerun Step 4a to validate and reuse it.\n- Never create an ordinary copied field without an explicit refresh owner.\n\nReference:\nhttps:\u002F\u002Flearn.microsoft.com\u002Fpower-apps\u002Fdeveloper\u002Fdata-platform\u002Fspecialized-columns\n\n### Step 5d — Create alternate keys for unique business identifiers\n\n**Print before starting:**\n> \"→ Creating alternate keys for columns marked unique in the data model (one HTTP call per key). Skip if no unique columns are planned.\"\n\n**Run condition:** the `## Data Model` section marks a non-primary column as unique \u002F alternate key \u002F natural key. Common examples: QR Code Value, SKU, external ID, employee number, asset tag. Skip primary IDs and skip columns whose type Dataverse cannot index as an alternate key (file\u002Fimage, memo\u002Flong text, multi-select choice, calculated\u002Frollup, customer\u002Fowner lookups).\n\n**Ordering:** run after the target table and target columns exist, and before Step 6b publish. Alternate-key index activation is asynchronous; creation may return success while the key status is `Pending`.\n\n**Do NOT use the `CreateEntityKey` action route.** In practice it can return 404 depending on route shape \u002F environment. The reliable metadata route is POSTing to the table's `Keys` navigation collection:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> POST \\\n  \"EntityDefinitions(LogicalName='\u003Ctable>')\u002FKeys\" \\\n  --body '\u003Centity-key-json>' \\\n  --solution '\u003Csolution-uniquename-from-memory-bank>' \\\n  --tenant-id '\u003CtenantId-from-resolve-environment>'\n```\n\nBody skeleton:\n\n```json\n{\n  \"@odata.type\": \"Microsoft.Dynamics.CRM.EntityKeyMetadata\",\n  \"SchemaName\": \"\u003Cprefix>_\u003Ctable>_\u003Ccolumn>_key\",\n  \"DisplayName\": { \"@odata.type\": \"Microsoft.Dynamics.CRM.Label\", \"LocalizedLabels\": [{ \"Label\": \"\u003CColumn display> Key\", \"LanguageCode\": 1033 }] },\n  \"KeyAttributes\": [\"\u003Ccolumn_logical_name>\"]\n}\n```\n\n**Pre-flight each key before POST** so re-runs are idempotent. A key can only pre-exist on a table that already existed at Step 4, so for a table created in this run, skip straight to the POST. Otherwise read `Keys` from that table's Step 4 snapshot. Query it directly only when the snapshot did not cover that table:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> GET \\\n  \"EntityDefinitions(LogicalName='\u003Ctable>')?\\$select=LogicalName&\\$expand=Keys(\\$select=SchemaName,KeyAttributes,EntityKeyIndexStatus)\" \\\n  --tenant-id '\u003CtenantId-from-resolve-environment>'\n```\n\n| Existing key state | Action |\n|---|---|\n| Same `SchemaName` or same `KeyAttributes` exists with `Active` \u002F `Pending` | Skip POST; record the key in `.datamodel-manifest.json`. |\n| Same `SchemaName` exists with `Failed` | Surface the failure and stop; Dataverse requires deleting\u002Frecreating the key manually or changing the planned key name. |\n| No matching key | POST to `EntityDefinitions(LogicalName='\u003Ctable>')\u002FKeys`. |\n\n**After POST:** a `204` response is success. Re-query the `Keys` expand above and capture `EntityKeyIndexStatus`. If it is `Pending`, continue the scaffold but add a memory-bank follow-up: `alternate key \u003Cschema> pending index activation`. Do not rely on duplicate enforcement in manual tests until the status reaches `Active`.\n\nAdd alternate keys to `.datamodel-manifest.json` for the table:\n\n```json\n\"alternateKeys\": [\n  { \"schemaName\": \"cr123_item_code_key\", \"keyAttributes\": [\"cr123_code\"], \"indexStatus\": \"Pending\" }\n]\n```\n\n### Step 6 — Add data sources\n\n**Telemetry checkpoint: `generate_dataverse_data_sources`**\n\n**Print before starting:**\n> \"→ Generating TypeScript services for \u003CN> tables via `pa app add data-source` (sequential). Print '✓ \u003Ctable>Service.ts' after each.\"\n\nWhen `\u003Coperation_manifest_mode> = valid`, set `SERVICE_REQUIRED_TABLES` from\n`service.requiredTables[].logicalName`. Keep the manifest's resolved adapted\nnames and exclude only explicit deferred rows. Service generation remains\nsequential outside BATCH-METADATA.\n\nFor each table in `SERVICE_REQUIRED_TABLES` (regardless of reuse\u002Fextend\u002Fcreate),\nverify its registered service before deciding whether generation is needed.\nPreserve verified unchanged services outside the approved delta. For an approved\nrefresh or a service affected by this run's approved schema changes, use\n[Refresh a retained source](..\u002F..\u002Fshared\u002Freferences\u002Fdata-source-removal.md#refresh-a-retained-source)\nwith the exact existing registration. Only an approved missing binding takes\nthe add command below. A required unregistered service outside approved scope\nreturns `NEEDS_CONTEXT`, not permission to register unrelated tables.\nDo not derive the service list from Creation Order alone because reused tables\nare intentionally absent from creation tiers. The CLI reads the environment ID\nfrom `power.config.json`; pass the environment URL resolved earlier in the skill:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\n$PA app add data-source --connector dataverse --org-url \u003CenvUrl> --table \u003Ctable-logical-name>\n```\n\nRun **one at a time — sequentially**, not in parallel. The Power Apps CLI writes `src\u002Fgenerated\u002FconnectorSchemas.ts` and other generated files non-atomically; concurrent invocations corrupt them.\n\nAfter generation, verify the output created by\n`pa app add data-source` rather than guessing a JSON path or service\nfilename. The command writes Dataverse configuration under the literal\n`databaseReferences[\"default.cds\"].dataSources` key and derives service\nfilenames from each entry's `entitySetName`, which may differ from the table\nlogical name. The verifier also accepts the legacy nested\n`databaseReferences.default.cds` shape:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fverify-dataverse-services.js\" \\\n  --project-root '\u003Cworking_dir>' \\\n  --manifest \"$OPERATION_MANIFEST\"\n```\n\nFor the standalone fallback path without a manifest, pass the resolved service\nlist as `--tables \"\u003Ccomma-separated-logical-names>\"`. If any reused or custom\ntable is missing from config, lacks `entitySetName`, or has no matching generated\nservice, STOP before screen generation:\n\n```text\nBLOCKED: required Dataverse service missing for \u003Clogical-name>. Schema action=\u003Creuse|extend|create>; app usage=\u003Cscreens\u002Fhooks that require it>.\n```\n\n### Step 6b — Publish customizations\n\n**Telemetry checkpoint: `publish_dataverse_customizations`**\n\nWhen `\u003Coperation_manifest_mode> = valid`, skip this step: the validated\nmanifest's final `publish` phase already ran and its pending checkpoint was\ndeleted, or the manifest proved there were zero metadata writes and no pending\npublish retry.\n\n**Print before starting:**\n> \"→ Publishing customizations (PublishXml) so new tables\u002Fcolumns become queryable. ~5–20 seconds.\"\n\nOnly after **every** Step 5 metadata POST and **every** Step 6 `pa app add data-source` has returned successfully, publish so the new tables and columns are available to the runtime. `PublishXml` takes the same exclusive metadata lock as the create\u002Fextend calls — do not run it concurrently with anything from Steps 5 or 6.\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> POST \\\n  \"PublishXml\" \\\n  --body \"{\\\"ParameterXml\\\":\\\"\u003Cimportexportxml>\u003Centities>\u003Centity>cr123_table1\u003C\u002Fentity>\u003Centity>cr123_table2\u003C\u002Fentity>\u003C\u002Fentities>\u003C\u002Fimportexportxml>\\\"}\" \\\n  --tenant-id '\u003CtenantId-from-resolve-environment>'\n```\n\nBuild the entity list from all tables that were **created or extended** in Steps 4–5. Skip reused-as-is tables — they don't need republishing.\n\nIf the publish call returns a non-2xx status, report the error and stop — do not proceed. The user must resolve before the tables are usable.\n\nAfter a 2xx publish, invalidate the planning-only inventory cache before\nverification so a later planning run cannot reuse pre-publication inventory:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nif ! node \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-inventory-cache.js\" \\\n  --file '\u003Cworking_dir>\u002F.tmp\u002Fdataverse-inventory-cache.json' --invalidate; then\n  printf 'NEEDS_RECOVERY: dataverse-inventory-cache\\n' >&2\n  exit 2\nfi\n```\n\nApply the same cache-invalidation recovery rule from the validated publish path\nabove. Continue to Step 6c only after invalidation exits `0`.\n\n### Step 6c — Verify tables exist\n\n**Telemetry checkpoint: `verify_dataverse_schema`**\n\nConfirm every table in `SERVICE_REQUIRED_TABLES`, including reused-as-is tables,\nplus any created or extended table, is queryable after publication when needed.\nUse the resolved effective names from the approved operation manifest or the\nstandalone path's verified service list, not Creation Order alone. Use **one**\nfiltered query for the bounded set, not one request per table:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Fdataverse-request.js\" \u003CenvUrl> GET \\\n  \"EntityDefinitions?\\$select=LogicalName,DisplayName&\\$filter=LogicalName eq '\u003Ctable1>' or LogicalName eq '\u003Ctable2>'\" \\\n  --tenant-id '\u003CtenantId-from-resolve-environment>'\n```\n\n- **Every expected name present in `value[]`** → confirmed.\n- **Any expected name missing** → schema\u002Fservice verification is incomplete.\n  Return to the owning reconciliation step; do not mark that table materialized\n  or invent a new table to replace a missing reuse target.\n\n### Step 6d — Write `.datamodel-manifest.json`\n\nAfter schema verification in Step 6c and generated-service verification in\nStep 6 both pass, write the manifest to the project root using the `Write`\ntool. This is the application's verified table inventory, not a schema-write\nlog; a reuse-only app must still have a populated manifest:\n\n```json\n{\n  \"environmentUrl\": \"\u003CenvUrl>\",\n  \"generatedAt\": \"\u003CISO timestamp>\",\n  \"tables\": [\n    {\n      \"logicalName\": \"cr123_jobsite\",\n      \"displayName\": \"Job Site\",\n      \"status\": \"new\",\n      \"metadataId\": \"\u003Cserver-assigned GUID from Step 5a re-GET>\",\n      \"solution\": \"\u003Csolution unique name, e.g. PowerAppsDefault>\",\n      \"columns\": [\n        { \"logicalName\": \"cr123_sitename\", \"type\": \"String\" },\n        { \"logicalName\": \"cr123_address\",  \"type\": \"String\" }\n      ]\n    }\n  ]\n}\n```\n\n`metadataId` and `solution` are required for `status: \"new\"` or `\"extended\"` entries — they're how Step 5a distinguishes \"we own this on a re-run\" from \"name collision.\" Reused tables can omit both.\n\nInclude every service-required table confirmed in Step 6c, including tables\nreused with no schema changes, plus verified new\u002Fextended tables. Use\n`status: \"reused\"` for unchanged existing tables; retain `\"new\"` and\n`\"extended\"` only for their actual outcomes. Preserve effective adapted names,\nentity-set names, and verified column\u002Frelationship\u002Fkey facts from reconciliation\nand any post-write metadata reads. Exclude deferred or unverified tables.\n\nBefore returning, check that every `SERVICE_REQUIRED_TABLES` name occurs in the\nmanifest and has verified generated service output. A partial or empty manifest\nfrom an earlier run is repaired by repeating Step 6's read-only service verifier\nand Steps 6c–6d, not by replaying successful schema writes. Recording a reused\ntable does not authorize a metadata POST or republish; the existing sample-data\nrecord-count checks and standard-system-table exclusions still apply.\n\nDuring an edit with pending removals, preserve retiring entries until the\nremoval branch verifies that their app bindings\u002Fservices are gone. The owner\nthen reconciles the final inventory; a shortened plan alone is not cleanup.\nReturn the actual created\u002Fextended\u002Freused table sets for this invocation to the\nuser or current owner; historical manifest status is not evidence of creation\nin this invocation. Seeding must use an\nexplicit approved allowlist excluding retirements, never this transitional\ninventory as its insertion scope.\n\n### Step 7 — Inspect generated files\n\n```text\nGlob: \u003Cworking_dir>\u002Fsrc\u002Fgenerated\u002Fservices\u002F*Service.ts\nGlob: \u003Cworking_dir>\u002Fsrc\u002Fgenerated\u002Fmodels\u002F*Model.ts\n```\n\nFor each table, check the generated service exposes the expected methods:\n\n```text\nGrep pattern=\"async (create|getAll|getById|update|delete|upload|downloadFile|downloadImage)\" path=\"\u003Cworking_dir>\u002Fsrc\u002Fgenerated\u002Fservices\u002F\u003CTable>Service.ts\"\n```\n\nIf the table has file or image columns, confirm the service includes `upload`, `downloadFile`, `downloadImage`, `deleteFileOrImage` — and the model exposes `\u003CTable>FileColumnName` \u002F `\u003CTable>ImageColumnName` union types.\n\n**File\u002Fimage column UI controls:** When a generated table has File or Image columns, note this in the summary so screen-builders apply the host controls from `@microsoft\u002Fpower-apps-native-host`:\n- **File columns** → `\u003CFilePicker>`; upload bytes separately via the generated service's upload method after the main create\u002Fupdate.\n- **Image columns** → `\u003CImagePicker>`; capture `PickedImageInfo` via `onImageChange` and persist through generated `upload(...)` after the main create\u002Fupdate.\n- **Read\u002Fview flows** → use generated `downloadFile(...)` \u002F `downloadImage(...)` helpers for existing attachments\u002Fpreviews.\n\nFull usage pattern and the native-wrapper boundary live in [`\u002Fadd-native`](..\u002Fadd-native\u002FSKILL.md#fileimage-picker-ownership); screen-builder keeps only the concise JSX enforcement rule.\n\n**PDF\u002Fsignature artifact schema guidance:** If the approved plan mentions generated PDFs, PDF evidence packets, approvals, signatures, sign-off, ink, or drawings, preserve the storage decision in the Dataverse model instead of defaulting to text fields.\n\n| User need | Dataverse shape | Write pattern |\n|---|---|---|\n| Generated PDF report that must be retained | File column on the parent record, or child Evidence\u002FAttachment table with a File column | Create\u002Fupdate parent row first, then call generated `Service.upload(parentId, '\u003CfileColumn>', file)` |\n| Generated PDF report that is only transient | No Dataverse column required | Generate locally with `expo-print` only when present; share with `expo-sharing` only when present; do not route local URI to native PDF viewer |\n| Captured signature\u002Fsign-off image | Image column when the latest signature belongs on the parent row | Strip `data:image\u002Fpng;base64,` if the generated service expects raw base64, then include image payload in the update body |\n| Multiple signatures, sketches, evidence images, or audit attachments | Child Evidence\u002FAttachment table with Image\u002FFile columns and lookup to parent | Create child row first, then include Image payload or upload File bytes through generated service helpers |\n\nSignature image normalization example:\n\n```ts\nconst signatureBase64 = signatureDataUri.replace(\u002F^data:image\\\u002Fpng;base64,\u002F, '');\nconst result = await Cr123_approvalService.update(approvalId, {\n  cr123_signatureimage: signatureBase64,\n  cr123_signedat: new Date().toISOString(),\n});\n\nif (!result.success) {\n  throw new Error(result.error?.message ?? 'Signature image was not saved.');\n}\n```\n\nFile upload after parent row exists example:\n\n```ts\nconst save = await Cr123_inspectionService.update(inspectionId, {\n  cr123_reportgeneratedat: new Date().toISOString(),\n});\n\nif (!save.success) {\n  throw new Error(save.error?.message ?? 'Inspection was not saved.');\n}\n\nconst upload = await Cr123_inspectionService.upload(inspectionId, 'cr123_reportfile', reportFile);\n\nif (!upload.success) {\n  throw new Error(upload.error?.message ?? 'Inspection report was not uploaded.');\n}\n```\n\n### Step 8 — Type-check\n\n**Telemetry checkpoint: `validate_dataverse_integration`**\n\n**Print before starting:**\n> \"→ Regenerating connector schemas + running tsc to verify generated services compile (~15–30 seconds).\"\n\n`pa app add data-source` (Step 5) wrote new files into `.power\u002Fschemas\u002F\u003Cconnector>\u002F`. The `connectorSchemas.ts` consumed by `app\u002F_layout.tsx` is now stale — regenerate it before type-checking, otherwise the new tables won't be wired into the runtime schema map and `tsc` will pass against an out-of-date snapshot:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnpm run generate-schemas\nnpx --no-install tsc --noEmit\n```\n\nFix any errors. Common: missing peer dependencies — `npx --no-install expo install \u003Cpackage>`.\n\n### Step 8.5 — Offline profile reconciliation\n\nA schema change here (new table or new column) can leave an existing Mobile Offline Profile behind — new tables never sync to devices and new columns come down blank. Reconcile the profile with what you just created.\n\n**Skip this step only for a valid scoped orchestrator handoff with\n`--skip-planning`** whose owner explicitly takes responsibility for offline\nreconciliation, as `\u002Fcreate-mobile-app` and `\u002Fsetup-datamodel` do in their existing\nflows. Return the verified delta to that owner without double-prompting.\nThe flag alone must not suppress standalone reconciliation.\n\nOtherwise (manual `\u002Fadd-dataverse`), run the local, no-network delta check:\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode \"${PLUGIN_ROOT}\u002Fscripts\u002Foffline-profile-delta.js\"\n```\n\nBranch on the JSON `status` per [offline-profile-reconciliation.md](${PLUGIN_ROOT}\u002Fshared\u002Freferences\u002Foffline-profile-reconciliation.md):\n\n| `status` | Action |\n|---|---|\n| `no-manifest` \u002F `no-profile` \u002F `in-sync` | Continue to Step 9 silently. For `no-profile` (no offline profile exists) do not nag — the app may not use offline. |\n| `error` | `offline-profile.json` is unreadable — the script prints `status: error` and **exits non-zero**. Do NOT treat this as an `\u002Fadd-dataverse` failure (the tables are already created): surface the `error` string, **skip reconciliation** (never drive the update workflows against a corrupt file), and finish with `DONE_WITH_CONCERNS` telling the user to fix `offline-profile.json`. |\n| `delta` | Prompt the user (one `AskUserQuestion`, default = update now) to add the missing tables \u002F new columns. Use the reconciliation reference's **Scoped helper handoffs** with `orchestrator: add-dataverse`, the absolute `working_dir`, `phase: implementation`, and the approved environment\u002Fprofile\u002Ftable delta. For `missingTables[]`, read and execute `${PLUGIN_ROOT}\u002Fskills\u002Fadd-table-to-offline-profile\u002FSKILL.md` with `--working-dir '\u003Cworking_dir>' --table \u003Ct>`; for `tablesWithNewColumns[]`, read and execute `${PLUGIN_ROOT}\u002Fskills\u002Fedit-offline-profile\u002FSKILL.md` with those arguments plus `--columns add:\u003CnewColumns>`. Re-run the delta check using the reference's failure dispatch; it should read `in-sync`. |\n\n### Step 9 — Summary\n\n```\n✅ Dataverse added\n─────────────────────────────────────────────\nEnvironment   : \u003CenvUrl>\nTables reused : \u003Clist>\nTables extended: \u003Clist (columns added)>\nTables created : \u003Clist (in tier order)>\nAdapted       : \u003Crenamed table\u002Fcolumn → new name, and why. Omit the line if none.>\nDeferred      : \u003Citems left out of this run and why, e.g. \"cr123_note (target not customizable)\". Omit the line if none.>\nOperation manifest: \u003Cvalidated path, operation count, or \"standalone fallback\">\n\nGenerated services:\n  src\u002Fgenerated\u002Fservices\u002F\u003CTable>Service.ts × N\nGenerated models:\n  src\u002Fgenerated\u002Fmodels\u002F\u003CTable>Model.ts × N\n\nType-check: PASS\n\nSample usage:\n\n  import { Cr123_jobsiteService } from '..\u002F..\u002Fsrc\u002Fgenerated\u002Fservices\u002FCr123_jobsiteService';\n\n  const result = await Cr123_jobsiteService.getAll({\n    select: ['cr123_sitename', 'cr123_address'],\n    filter: 'statecode eq 0',\n    orderBy: ['cr123_sitename asc'],\n    top: 50,\n  });\n  const sites = result.data ?? [];\n\n⚠️  First call triggers Dataverse OAuth consent via the native player's\n    `\u003Cscheme>:\u002F\u002Foauth-callback` deep link.\n\nNext:\n  \u002Fadd-sample-data        # Seed each new table with 5-10 realistic rows so the\n                          # app's home screen shows real-looking data on first launch.\n─────────────────────────────────────────────\n```\n\nAfter printing the summary, **offer one-click sample-data seeding** — but only when invoked manually (not from `\u002Fcreate-mobile-app`, which handles this in its own Step 8.5).\n\n- **For a valid scoped orchestrator handoff with `--skip-planning`**: skip the\n  prompt. The orchestrator invokes `\u002Fadd-sample-data` separately.\n- **Otherwise (manual invocation)**, propose exact verified, non-retiring seed\n  targets from this operation and a count policy; do not select every table\n  just because it appears in the manifest. Ask:\n\n  > \"Seed these \u003CN> tables with sample records using the proposed counts? (yes \u002F no)\"\n\n  Only an explicit yes approves seeding. On no\u002Fcancel\u002Fdismissal, stop without\n  inserts; empty input is not consent. On yes, invoke `\u002Fadd-sample-data` with the\n  same absolute `--working-dir`, `--tables \"\u003Capproved-logical-names>\"`, and\n  `--exclude-tables \"\u003Cretiring-logical-names-or-empty>\"`. Carry the specific\n  approval forward; do not broaden it if lookups need additional parents.\n\n## Key Rules\n\n- **Always** use generated services (e.g., `Cr123_jobsiteService.getAll()`) — never `fetch` \u002F `axios` directly.\n- Result data lives at `result.data`, not `result` itself.\n- Don't edit files under `src\u002Fgenerated\u002F` — they are regenerated on every `pa app add data-source`.\n- Picklist (Choice) fields, virtual fields, lookups, and file\u002Fimage columns each have non-obvious gotchas. Keep `references\u002Fdataverse-reference.md` aligned with this skill.\n- A valid operation manifest removes repeated agent reconciliation; it does not\n  change Dataverse's serialized metadata-lock latency. Real matched A\u002FB runs\n  remain required before claiming an end-to-end timing range.\n- **When a Dataverse Web API behavior is uncertain (lookup write syntax, `$expand` nav property names, choice column shape, batch semantics, error format), query the `microsoft-learn` MCP server before guessing.** See [shared\u002Fshared-instructions.md → Microsoft Learn MCP](..\u002F..\u002Fshared\u002Fshared-instructions.md#microsoft-learn-mcp-authoritative-microsoft-docs). Guessed Dataverse syntax silently 400s.\n\n## Reference\n\n- [`scripts\u002Fdataverse-request.js`](..\u002F..\u002Fscripts\u002Fdataverse-request.js) — bundled in this plugin\n- [`scripts\u002Fbuild-dataverse-operation-manifest.js`](..\u002F..\u002Fscripts\u002Fbuild-dataverse-operation-manifest.js) — deterministic contract\u002Fsnapshot reconciliation, binding, validation, and ordered BATCH-METADATA phase generation\n- [shared\u002Freferences\u002Foffline-profile-reconciliation.md](..\u002F..\u002Fshared\u002Freferences\u002Foffline-profile-reconciliation.md) — Step 8.5 offline delta check + reconciliation flow\n",{"name":5,"model":31,"description":7,"allowed-tools":49,"user-invocable":50},"Read, Edit, Write, Grep, Glob, Bash, AskUserQuestion, EnterPlanMode, ExitPlanMode, Task, Skill",true,"main","0a55a88fa2855404029d1e5450faf92878baf763","plugins\u002Fmobile-apps\u002Fskills\u002Fadd-dataverse",232,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills","https:\u002F\u002Fmicrosoft.github.io\u002Fpower-platform-skills\u002F","https:\u002F\u002Fgithub.com\u002Fmicrosoft","https:\u002F\u002Fopensource.microsoft.com","Open source projects and samples from Microsoft","2026-11-14 21:46:39",{"source":8,"skillName":5,"commands":62,"prompt":72,"zipAvailable":50},[63,66,69],{"tool":64,"command":65},"npx","npx skills add microsoft\u002Fpower-platform-skills --skill add-dataverse",{"tool":67,"command":68},"bunx","bunx skills add microsoft\u002Fpower-platform-skills --skill add-dataverse",{"tool":70,"command":71},"pnpm","pnpm dlx skills add microsoft\u002Fpower-platform-skills --skill add-dataverse","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fmobile-apps\u002Fskills\u002Fadd-dataverse\u002FSKILL.md",[74],{"path":75,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":78,"sourceHtmlUrl":79},"references\u002Fdataverse-reference.md","REFERENCE","参考文档",28501,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fmobile-apps\u002Fskills\u002Fadd-dataverse\u002Freferences\u002Fdataverse-reference.md",[81,101,123,141,155,165],{"skillNo":82,"name":83,"nameZh":6,"description":84,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":85,"categories":86,"keywords":87,"publishedAt":44},"01M699WVHX0XN59EPBTTPEKYPG","setup","Use when Process Intelligence needs building, explicit cloud\u002Faccount\u002Fenvironment configuration, sign-in, connection diagnostics, or recovery from an authentication error.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fprocess-intelligence\u002Fskills\u002Fsetup\u002FSKILL.md",[],[83,40,88,89,90,91,92,93,36,94,95,96,97,98,99,100],"when","connection","error","sign","intelligence","configuration","process","diagnostics","environment","authentication","cloud","recovery","Bash",{"skillNo":102,"name":103,"nameZh":6,"description":104,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":105,"categories":106,"keywords":112,"publishedAt":44},"01M699WSR9EX55YY1WD9J22FEG","report-issue","Use when the user wants a Process Intelligence bug-report draft or support summary for a failed operation.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fprocess-intelligence\u002Fskills\u002Freport-issue\u002FSKILL.md",[107],{"categoryNo":108,"categoryName":109,"taxonomyType":110,"taxonomyTypeDesc":111},"01JBSEEDCAT000000000000034","行政与办公人员","PROFESSION","适用职业",[113,114,115,116,37,94,36,117,118,92,119,120,121,88,122],"report issue","report","issue","support","bug","summary","draft","operation","for","bug-report",{"skillNo":124,"name":125,"nameZh":6,"description":126,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":127,"categories":128,"keywords":129,"publishedAt":44},"01M699WR26YQ9Q21KE6MJF5B2G","investigate-process","Use when a broad Process Mining business question, observed pattern or competing explanation needs investigation across analytical perspectives.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fprocess-intelligence\u002Fskills\u002Finvestigate-process\u002FSKILL.md",[],[94,130,131,132,88,133,134,135,136,40,137,36,138,139,140],"investigate","investigate process","mining","process mining","investigation","perspectives","question","business","pattern","across","analytical",{"skillNo":142,"name":143,"nameZh":6,"description":144,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":145,"categories":146,"keywords":147,"publishedAt":44},"01M699WP9SDG5ZR12P8VYNXKTN","derive-metric","Use when a requested measure is missing, a saved custom metric needs reuse, or an ephemeral formula or derived scalar is needed.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fprocess-intelligence\u002Fskills\u002Fderive-metric\u002FSKILL.md",[],[148,149,40,36,150,151,152,153,154,88],"metric","derive metric","formula","missing","reuse","saved","custom",{"skillNo":156,"name":157,"nameZh":6,"description":158,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":159,"categories":160,"keywords":161,"publishedAt":44},"01M699WMG5R8P6SMTDAS1TKF9E","compare-cohorts","Use when comparing process cohorts, departments, periods or before-and-after results under matched definitions.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fprocess-intelligence\u002Fskills\u002Fcompare-cohorts\u002FSKILL.md",[],[162,163,88,36,94,164,40],"compare","compare cohorts","results",{"skillNo":166,"name":167,"nameZh":6,"description":168,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":169,"categories":170,"keywords":171,"publishedAt":44},"01M699WJWNVTMP9XK3NCJHV14D","analyze-variants","Use when investigating common or slow variants, repeated work, unusual paths, or a specific Process Mining case.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fprocess-intelligence\u002Fskills\u002Fanalyze-variants\u002FSKILL.md",[],[172,173,174,133,88,132,175,176,36,94,40,177,178],"analyze","variants","analyze variants","case","paths","work","common"]