add-table-to-offline-profile

作者: microsoft

当用户希望将一张表(通常是新添加的Dataverse表)添加到现有离线配置文件中,而无需重新运行完整…

npx skills add https://github.com/microsoft/power-platform-skills --skill add-table-to-offline-profile

Shared instructions: shared-instructions.md — read first.

References:

Add Table to Offline Profile

Add a single table to an existing Mobile Offline Profile. Most common flow: user ran /add-dataverse to add a new table to their app, now wants that table available offline too.

Equivalent to running /setup-offline-profile and seeing the existing profile (extend-mode), but skips the per-table questionnaire for the tables already in the profile — only configures the new one.

Invocation context and root binding

This internal helper is always a child invocation. Before any project read or command, execute app-working-directory.md. Require the owner's absolute working_dir; --working-dir, when supplied, must identify that same native-canonical root. Missing, relative, or conflicting root context returns NEEDS_CONTEXT before project access; an inaccessible directory is BLOCKED. Never use the launch cwd or introduce another root resolver.

Receive MOBILE_APP_ORCHESTRATING=1, orchestrator, working_dir, phase, approved_scope, and supplied answers from the owner. Keep the selected environment ID/URL/tenant, profile ID, exact table allowlist, and approved operations unchanged. Missing or conflicting scope returns NEEDS_CONTEXT; do not substitute another environment, profile, or table. The marker is not approval: schema approval alone does not authorize offline prerequisite changes, profile items, relationships, columns, publication, deletion, or membership edits. Reuse only the same current offline approvals; otherwise retain Steps 3–4's gates. For --plan-only or a planning-phase handoff, return a proposal after permitted read-only discovery, before any prerequisite update, profile mutation, publish, or artifact write. A gate response cannot lift proposal-only mode.

Apply the canonical contract to every shell call and file tool, including version/auth checks, row counts, screen greps, commands copied from references, verification, recovery, and retries. Re-supply the absolute PLUGIN_ROOT and required context on each call; no prior cd, export, or shell variable persists. Use the canonical Bash guard below or its PowerShell equivalent. Replace placeholders with validated, shell-quoted invocation values; missing values return NEEDS_CONTEXT, never execute an unresolved placeholder. File tools use absolute <working_dir>/... paths; plugin reads/scripts use ${PLUGIN_ROOT}/.... Resolve manifest location once within this root and keep that absolute path; never search another app when a file is absent.

Workflow

  1. Verify project + locate profile → 2. Resolve target table → 3. Prereq check (auto-enable if needed) → 4. Scope picker (single question) → 5. POST item + PATCH selectedcolumns → 6. Publish → 7. Update artifacts → 8. Summary

Step 1 — Verify project + locate profile

cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
if [ ! -f power.config.json ] || [ ! -f app.config.js ]; then
  echo "BLOCKED: working_dir is not an initialized app" >&2
  exit 1
fi

Read <working_dir>/power.config.json and require a nonempty environmentId matching the owner's selected environment. Reuse matching complete environment context, or perform this non-persisting lookup from the bound root:

cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
environment_id=$(node -p "require('./power.config.json').environmentId || ''") || {
  echo "BLOCKED: unreadable power.config.json" >&2; exit 1;
}
if [ -z "$environment_id" ] || [ "$environment_id" != "<selected-environment-id>" ]; then
  echo "NEEDS_CONTEXT: selected environment does not match working_dir" >&2
  exit 1
fi
node "${PLUGIN_ROOT}/scripts/resolve-environment.js" "$environment_id" --no-cache --require-tenant

Require the returned environment ID, HTTPS URL, and tenant to match the owner; incomplete or conflicting context returns NEEDS_CONTEXT. Never drop --no-cache --require-tenant, change configuration, or switch auth/environment to recover a failure. Use that exact <envUrl> and <tenantId> for every request.

Manifest path (dual-location, both within the bound root):

cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
if [ -f .datamodel-manifest.json ]; then
  printf '%s\n' '<working_dir>/.datamodel-manifest.json'
elif [ -f docs/plan-artifacts/.datamodel-manifest.json ]; then
  printf '%s\n' '<working_dir>/docs/plan-artifacts/.datamodel-manifest.json'
else
  echo "NEEDS_CONTEXT: no manifest in working_dir" >&2
  exit 1
fi

Read the profile from <working_dir>/offline-profile.json or the owner's explicit --profile-id, using /edit-offline-profile's resolution rules inline. Verify it matches the owner's profile and the live profile in the selected environment. A conflicting snapshot/argument, 404, or permission failure is not permission to pick a different profile; return NEEDS_CONTEXT for conflict or BLOCKED for access failure. Do not execute the edit workflow just to locate a profile.

STOP if no profile exists. Recommend: Run /setup-offline-profile first to create the profile.

Step 2 — Resolve target table

$ARGUMENTS parsing:

FlagEffect
--table <logical-name>Explicit target
--all-newAdd missing manifest tables only within the owner's exact table allowlist (bulk mode); never widen to unrelated pending tables
(no flags)Interactive: list allowed tables in the manifest NOT yet in the selected profile via AskUserQuestion (max 4); if more than 4 candidates, prompt user to specify by name in next message

An explicit --table must match the owner's allowlist. Conflicting selection returns NEEDS_CONTEXT. For bulk --all-new mode, loop through Steps 3–7 for each approved missing table; each runs sequentially because Dataverse profile-item POSTs serialize. Every iteration keeps the same root/environment/profile context.

Step 3 — Prereq check (auto-enable if needed)

Query the target table's EntityMetadata:

cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" "<envUrl>" GET \
  "EntityDefinitions(LogicalName='<table>')?\$select=IsAvailableOffline,ChangeTrackingEnabled,OwnershipType,IsCustomizable" \
  --tenant-id "<tenantId>"
Flags stateAction
Both already trueSkip to Step 4
Either false AND IsCustomizableUse update-entity-offline-flags.js only if the current offline approval covers these exact prerequisite changes and publication; otherwise show the changes and obtain owner/user approval first
IsCustomizable.Value = falseSTOP with BLOCKED: <table> is system-managed and cannot be flagged for offline. Use a different table or accept this row is read-only-offline.

After approval, execute the helper for this table only with the resolved tenant. --tenant-id binds both initial authentication and 401 refresh; never rely on ambient tenant variables for this prerequisite mutation.

cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node "${PLUGIN_ROOT}/scripts/update-entity-offline-flags.js" "<envUrl>" \
  --table "<table>" --offline true --tracking true --tenant-id "<tenantId>"

Check the returned HTTP status, not just process exit. A 403/PrivilegeCheckFailed is BLOCKED; do not change identity or profile to bypass permissions. After a successful change, issue the approved prerequisite publication:

cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" "<envUrl>" POST \
  "PublishXml" --tenant-id "<tenantId>" --body '{
    "ParameterXml": "<importexportxml><entities><entity><table></entity></entities></importexportxml>"
  }'

Publish only this helper's changed prerequisite table. Do not use PublishAllXml unless the owner separately approves publishing all pending environment customizations; table-only approval does not cover that fallback.

No prerequisite mutation or publication is allowed in proposal-only mode.

Step 4 — Scope picker (single question)

Pull from the bound absolute manifest the target table's lookups[] to inform defaults. Run a root-bound row-count probe in the selected environment for this table only to inform reference-data classification.

AskUserQuestion with 4 options reflecting the architect's priority cascade:

Option labelMaps to
Organization rows — User's rows only (Recommended for most tables)recorddistributioncriteria: 2, recordsownedbyme: true
All records (for small reference data)recorddistributioncriteria: 1
Related rows only (for pure child tables)recorddistributioncriteria: 0
Custom — specify exact flags in next messagePrompt user with text

Recommendation in the question body should be derived from the architect's heuristics for the table being added (use the same priority cascade from offline-profile-architect.md Step 4 inline). The user can override.

Show the proposed relationships, selected columns, and sync interval alongside the scope choice. Reuse supplied answers only when they approve this exact offline change; otherwise wait for the choice before Step 5. Cancel stops without creating a profile item. Neither invocation nor the orchestration marker answers this gate.

Step 5 — POST item + associations + PATCH selectedcolumns

Step 5a — POST profile item

cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" "<envUrl>" POST \
  "mobileofflineprofileitems" --tenant-id "<tenantId>" \
  --body '{
    "name": "<DisplayName>",
    "regardingobjectid@odata.bind": "/mobileofflineprofiles(<profileId>)",
    "selectedentitytypecode": "<table>",
    "recorddistributioncriteria": <0|1|2>,
    "recordsownedbyme": <bool>,
    "recordsownedbymyteam": false,
    "recordsownedbymybusinessunit": false,
    "getrelatedentityrecords": true,
    "syncintervalinminutes": 10
  }' \
  --include-headers

Capture itemId from OData-EntityId response header.

Step 5b — POST associations for the new table's relationships

After the new item is created, walk the new table's relationships (from the manifest's lookups[] + a EntityDefinitions(LogicalName='<table>')/ManyToOneRelationships query) and POST one mobileofflineprofileitemassociation per relationship whose target is ALREADY in the profile.

Recipe per shared/references/dataverse-offline-api.md §5–§6:

cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" "<envUrl>" POST \
  "mobileofflineprofileitemassociations" --tenant-id "<tenantId>" \
  --body '{
    "name": "<relationshipSchemaName>",
    "relationshipdisplayname": "<relationshipSchemaName>",
    "relationshipid": "<MetadataId-from-EntityDefinitions>",
    "regardingobjectid@odata.bind": "/mobileofflineprofileitems(<newItemId>)"
  }' \
  --include-headers

Critical: do NOT include selectedrelationshipsschema in the body — server fills it in (empirical 2026-05-24).

For new tables that are recorddistributioncriteria: 0 (Related rows only), at least ONE inbound relationship MUST be included or the table will sync zero rows. The skill should validate this and warn if no associations are being created for a Related-only-scoped table.

Step 5c — PATCH selectedcolumns

Build selectedcolumns using the deterministic union from offline-profile-architect.md Step 6 (always-include ∪ lookups ∪ screen-grep'd, dedupe, sort). Scope file-tool greps to absolute <working_dir>/app/ and <working_dir>/src/ paths and the selected table; use the same proposed columns approved in Step 4.

cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" "<envUrl>" PATCH \
  "mobileofflineprofileitems(<itemId>)" --tenant-id "<tenantId>" \
  --body '{"selectedcolumns":"{\"Columns\":[...]}"}'

Step 6 — Publish (targeted PublishXml)

cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" "<envUrl>" POST \
  "PublishXml" --tenant-id "<tenantId>" --body '{
    "ParameterXml": "<publish><mobileofflineprofiles><mobileofflineprofile><profileId></mobileofflineprofile></mobileofflineprofiles></publish>"
  }'

Publishes only this profile, not the entire org's customizations. See shared/references/dataverse-offline-api.md §9 for 0x80071141 circular-relationship handling + PublishAllXml fallback.

All linked recovery commands, re-GET verification, and retries must bind the same root and pass the same environment/tenant/profile/table/item context. Removing a cycle association requires the explicit user decision in that recipe. An org-wide PublishAllXml fallback needs approval for that broader publication; do not silently escalate a profile-scoped approval. Never retry on a different profile or bypass a permission failure. Confirm publication before Step 7.

If a linked recovery recipe requires another offline helper, return the proposed change to the owner for approval first, then read the helper at its absolute ${PLUGIN_ROOT}/skills/<helper>/SKILL.md path. Forward:

Arguments: --working-dir '<working_dir>' --profile-id "<profileId>" --table "<table>" <exact-approved-change-flags>
Context: MOBILE_APP_ORCHESTRATING=1, orchestrator, working_dir, phase, approved_scope,
         selected environment ID/URL/tenant, profile ID, table allowlist, item ID,
         supplied answers, --plan-only (if present)

The recipient must enforce the same canonical root contract. Missing context returns NEEDS_CONTEXT; do not fall back to direct invocation or rediscovery.

Step 7 — Update artifacts

Append the new table's entry to <working_dir>/offline-profile.json tables[]. The entry MUST include a schemaColumns array — the added table's full set of schema column logical names from the bound absolute manifest at this moment. This is the schema-reconciliation baseline that ${PLUGIN_ROOT}/scripts/offline-profile-delta.js diffs future manifest changes against; omitting it reports columnBaselineMissing until re-reconciled. It is distinct from selectedColumns (the curated sync set) — see offline-profile-reconciliation.md. Match the canonical entry shape in /setup-offline-profile Step 9a without executing that workflow. Any delta check uses --project-root '<working_dir>' in its own guarded call.

Append to <working_dir>/memory-bank.md ## Offline profile block:

addedTables:
  - { at: 2026-05-19T..., table: <name>, itemId: <guid>, scope: <criterion> }

Step 8 — Summary

✓ Added <table> to profile.

  Profile      : <name>
  New item ID  : <guid>
  Scope        : <human-readable>
  Sync         : <minutes> min
  Columns      : <N> selected
  Published    : <ISO timestamp>

Total tables in profile now: <N>

Users who already have this profile assigned will receive the new table on their
next sign-in sync. To force-refresh, sign out and back in on the device.

Status code (final line)

  • DONE — table added, profile published, artifacts updated
  • DONE_WITH_CONCERNS: <list> — added with caveats (auto-enabled prereqs, publish timeout-then-success)
  • NEEDS_CONTEXT: <missing> — no profile to add to, or no target table
  • BLOCKED: <reason> — IsCustomizable=false on target, auth failure, POST/PATCH failure

What this skill does NOT do

  • Add N:N (ManyToMany) relationships to a profile item — uncommon, not covered by v0.1's recipe (which assumes N:1 / 1:N via the regardingobjectid lookup). If the user has an M:N use case, point them at the maker portal.
  • Re-architect existing tables in the profile — that's /edit-offline-profile.
  • Remove a table from the profile — manual DELETE /mobileofflineprofileitems(<itemId>) (no current skill).

来自 microsoft 的更多技能

oss-growth
microsoft
OSS增长黑客角色
agent-framework-azure-ai-py
microsoft
使用Microsoft Agent Framework Python SDK(agent-framework-azure-ai)构建Azure AI Foundry代理。在创建使用AzureAIAgentsProvider的持久化代理、使用托管工具(代码解释器、文件搜索、网络搜索)、集成MCP服务器、管理对话线程或实现流式响应时使用。涵盖函数工具、结构化输出和多工具代理。
development
airunway-aks-setup
microsoft
在AKS上设置AI Runway——从裸集群到运行模型。涵盖集群验证、控制器安装、GPU评估、提供商设置和首次部署。适用场景:“设置AI Runway”、“接入AKS集群”、“安装AI Runway”、“airunway设置”、“将模型部署到AKS”、“在AKS上进行GPU推理”、“在AKS上配置KAITO”、“在AKS上运行LLM”、“在AKS上使用vLLM”、“在AKS上设置模型服务”、“AI Runway控制器”。
devops
appinsights-instrumentation
microsoft
使用Azure Application Insights对Web应用进行插桩的指南。提供遥测模式、SDK设置和配置参考。适用场景:如何对应用进行插桩、App Insights SDK、遥测模式、什么是App Insights、Application Insights指南、插桩示例、APM最佳实践。
devops
applicationinsights-web-ts
microsoft
使用Application Insights JavaScript SDK(@microsoft/applicationinsights-web)为浏览器/Web应用添加检测。用于真实用户监控(RUM)——页面视图、点击、AJAX/fetch依赖项、异常、自定义事件,以及与后端OpenTelemetry追踪关联的浏览器端GenAI代理追踪。涵盖SDK加载器脚本和npm设置、框架扩展(React、React Native、Angular)、点击分析、遥测初始化器,以及从浏览器发出的代理/工具/模型跨度所遵循的OTel GenAI语义约定。
devops
azure-ai-anomalydetector-java
microsoft
使用适用于 Java 的 Azure AI 异常检测器 SDK 构建异常检测应用程序。在实现单变量/多变量异常检测、时间序列分析或 AI 驱动的监控时使用。
development
azure-ai-language-conversations-py
microsoft
使用azure-ai-language-conversations Python SDK实现对话语言理解(CLU)。当使用ConversationAnalysisClient分析对话意图和实体、构建NLP功能或将语言理解集成到应用程序中时使用。
development
azure-ai-ml-py
microsoft
Azure Machine Learning SDK v2 for Python。用于机器学习工作区、作业、模型、数据集、计算资源和管道。 触发词:“azure-ai-ml”、“MLClient”、“工作区”、“模型注册表”、“训练作业”、“数据集”。
development