"→ Generating TypeScript services for tables via npx power-apps add-data-source (sequential). Print '✓
Service.ts' after each."
For each table the app will use (regardless of reuse/extend/create), generate the TS layer from the app root. The CLI reads the environment ID from power.config.json; pass the environment URL resolved earlier in the skill:
npx power-apps add-data-source --api-id dataverse --org-url <envUrl> --resource-name <table-logical-name>
Run one at a time — sequentially, not in parallel. The Power Apps CLI writes src/generated/connectorSchemas.ts and other generated files non-atomically; concurrent invocations corrupt them.
Step 6b — Publish customizations
Print before starting:
"→ Publishing customizations (PublishXml) so new tables/columns become queryable. ~5–20 seconds."
Only after every Step 5 metadata POST and every Step 6 npx power-apps 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/extend calls — do not run it concurrently with anything from Steps 5 or 6.
node "${CLAUDE_SKILL_DIR}/../../scripts/dataverse-request.js" <envUrl> POST \
"PublishXml" \
--body "{\"ParameterXml\":\"<importexportxml><entities><entity>cr123_table1</entity><entity>cr123_table2</entity></entities></importexportxml>\"}"
Build 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.
If 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.
Step 6c — Verify tables exist
For each created or extended table, confirm it is queryable after publish:
node "${CLAUDE_SKILL_DIR}/../../scripts/dataverse-request.js" <envUrl> GET \
"EntityDefinitions(LogicalName='<table>')?\\$select=LogicalName,DisplayName"
- 200 → confirmed.
- 404 → table missing after publish — report and stop.
Step 6d — Write .datamodel-manifest.json
After all tables are verified, write the manifest to the project root using the Write tool:
{
"environmentUrl": "<envUrl>",
"generatedAt": "<ISO timestamp>",
"tables": [
{
"logicalName": "cr123_jobsite",
"displayName": "Job Site",
"status": "new",
"metadataId": "<server-assigned GUID from Step 5a re-GET>",
"solution": "<solution unique name, e.g. PowerAppsDefault>",
"columns": [
{ "logicalName": "cr123_sitename", "type": "String" },
{ "logicalName": "cr123_address", "type": "String" }
]
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.
Include only tables confirmed in Step 6c. Do NOT include tables reused with no schema changes.
Step 7 — Inspect generated files
Glob: src/generated/services/*Service.ts
Glob: src/generated/models/*Model.ts
For each table, check the generated service exposes the expected methods:
Grep pattern="async (create|getAll|getById|update|delete|upload|downloadFile|downloadImage)" path="src/generated/services/<Table>Service.ts"
If the table has file or image columns, confirm the service includes upload, downloadFile, downloadImage, deleteFileOrImage — and the model exposes <Table>FileColumnName / <Table>ImageColumnName union types.
File/image 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/power-apps-native-host:
- File columns →
<FilePicker>; upload bytes separately via the generated service's upload method after the main create/update.
- Image columns →
<ImagePicker>; capture PickedImageInfo via onImageChange and persist through generated upload(...) after the main create/update.
- Read/view flows → use generated
downloadFile(...) / downloadImage(...) helpers for existing attachments/previews.
Full usage pattern and the native-wrapper boundary live in /add-native; screen-builder keeps only the concise JSX enforcement rule.
PDF/signature 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.
| User need | Dataverse shape | Write pattern |
|---|
| Generated PDF report that must be retained | File column on the parent record, or child Evidence/Attachment table with a File column | Create/update parent row first, then call generated Service.upload(parentId, '<fileColumn>', file) |
| 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 |
| Captured signature/sign-off image | Image column when the latest signature belongs on the parent row | Strip data:image/png;base64, if the generated service expects raw base64, then include image payload in the update body |
| Multiple signatures, sketches, evidence images, or audit attachments | Child Evidence/Attachment table with Image/File columns and lookup to parent | Create child row first, then include Image payload or upload File bytes through generated service helpers |
Signature image normalization example:
const signatureBase64 = signatureDataUri.replace(/^data:image\/png;base64,/, '');
const result = await Cr123_approvalService.update(approvalId, {
cr123_signatureimage: signatureBase64,
cr123_signedat: new Date().toISOString(),
});
if (!result.success) {
throw new Error(result.error?.message ?? 'Signature image was not saved.');
}
File upload after parent row exists example:
const save = await Cr123_inspectionService.update(inspectionId, {
cr123_reportgeneratedat: new Date().toISOString(),
});
if (!save.success) {
throw new Error(save.error?.message ?? 'Inspection was not saved.');
}
const upload = await Cr123_inspectionService.upload(inspectionId, 'cr123_reportfile', reportFile);
if (!upload.success) {
throw new Error(upload.error?.message ?? 'Inspection report was not uploaded.');
}
Step 8 — Type-check
Print before starting:
"→ Regenerating connector schemas + running tsc to verify generated services compile (~15–30 seconds)."
npx power-apps add-data-source (Step 5) wrote new files into .power/schemas/<connector>/. The connectorSchemas.ts consumed by app/_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:
npm run generate-schemas
npx tsc --noEmit
Fix any errors. Common: missing peer dependencies — npx expo install <package>.
Step 8.5 — Offline profile reconciliation
A 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.
Skip this step entirely when $ARGUMENTS contains --skip-planning (the orchestrator-invoked path). /create-mobile-app, /setup-datamodel, and /edit-app own offline reconciliation in their own flow, so running it here too would double-prompt.
Otherwise (manual /add-dataverse), run the local, no-network delta check:
node "${CLAUDE_SKILL_DIR}/../../scripts/offline-profile-delta.js"
Branch on the JSON status per offline-profile-reconciliation.md:
status | Action |
|---|
no-manifest / no-profile / in-sync | Continue to Step 9 silently. For no-profile (no offline profile exists) do not nag — the app may not use offline. |
error | offline-profile.json is unreadable — the script prints status: error and exits non-zero. Do NOT treat this as an /add-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. |
delta | Prompt the user (one AskUserQuestion, default = update now) to add the missing tables / new columns. For missingTables[], read and execute ${CLAUDE_SKILL_DIR}/../add-table-to-offline-profile/SKILL.md; for tablesWithNewColumns[], read and execute ${CLAUDE_SKILL_DIR}/../edit-offline-profile/SKILL.md with --table <t> --columns add:<newColumns>. Re-run the delta check; it should read in-sync. Follow the exact prompt + ordering in the reconciliation reference. |
Step 9 — Summary
✅ Dataverse added
─────────────────────────────────────────────
Environment : <envUrl>
Tables reused : <list>
Tables extended: <list (columns added)>
Tables created : <list (in tier order)>
Generated services:
src/generated/services/<Table>Service.ts × N
Generated models:
src/generated/models/<Table>Model.ts × N
Type-check: PASS
Sample usage:
import { Cr123_jobsiteService } from '../../src/generated/services/Cr123_jobsiteService';
const result = await Cr123_jobsiteService.getAll({
select: ['cr123_sitename', 'cr123_address'],
filter: 'statecode eq 0',
orderBy: ['cr123_sitename asc'],
top: 50,
});
const sites = result.data ?? [];
⚠️ First call triggers Dataverse OAuth consent via the native player's
`<scheme>://oauth-callback` deep link.
Next:
/add-sample-data # Seed each new table with 5-10 realistic rows so the
# app's home screen shows real-looking data on first launch.
─────────────────────────────────────────────
After printing the summary, offer one-click sample-data seeding — but only when invoked manually (not from /create-mobile-app, which handles this in its own Step 8.5).
-
If $ARGUMENTS contains --skip-planning (the orchestrator-invoked path): skip the prompt. The orchestrator invokes /add-sample-data separately.
-
Otherwise (manual invocation), if the manifest contains any tables, ask:
"Seed tables with sample records so the app shows real-looking data on first launch? (yes / no — default: yes)"
Default to "yes" so empty input auto-proceeds. On "yes", invoke /add-sample-data. On "no", print "→ Skipped sample data. Run /add-sample-data later to populate." and stop.
Key Rules
- Always use generated services (e.g.,
Cr123_jobsiteService.getAll()) — never fetch / axios directly.
- Result data lives at
result.data, not result itself.
- Don't edit files under
src/generated/ — they are regenerated on every npx power-apps add-data-source.
- Picklist (Choice) fields, virtual fields, lookups, and file/image columns each have non-obvious gotchas. Keep
references/dataverse-reference.md aligned with this skill.
- 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/shared-instructions.md → Microsoft Learn MCP. Guessed Dataverse syntax silently 400s.
Reference