assign-offline-profile
Use when the user needs to bind users or teams to a Mobile Offline Profile so they actually receive offline sync on their devices. Without this, the profile exists in Dataverse but no one's app uses it.
git clone --depth 1 https://github.com/microsoft/power-platform-skills /tmp/assign-offline-profile && cp -r /tmp/assign-offline-profile/plugins/mobile-apps/skills/assign-offline-profile ~/.claude/skills/assign-offline-profileSKILL.md
**Shared instructions: [shared-instructions.md](${PLUGIN_ROOT}/shared/shared-instructions.md)** — read first.
**References:**
- [offline-profile-schema.md](${PLUGIN_ROOT}/shared/references/offline-profile-schema.md) — `usermobileofflineprofilemembership` / `teammobileofflineprofilemembership` entity field map
- [dataverse-offline-api.md](${PLUGIN_ROOT}/shared/references/dataverse-offline-api.md) — Web API recipe (§12 — membership POSTs)
# Assign Offline Profile
Bind one or more users and/or teams to an existing Mobile Offline Profile. Without this step, the profile exists in Dataverse but is unbound — no one's app actually uses it for offline sync.
Per the maker portal's UX (the "Assign profile to user" dialog under env settings), this is a separate operation from profile creation. Many users hit "I created the profile but offline still doesn't work" — the missing piece is membership.
## Workflow
1. Verify project + locate profile → 2. Pick users/teams → 3. Discover existing memberships → 4. Confirm diff (single gate) → 5. POST memberships → 6. Verify → 7. Summary
---
### Step 1 — Verify project + locate profile
```bash
test -f power.config.json
node "${PLUGIN_ROOT}/scripts/resolve-environment.js" "$(node -e \"console.log(require('./power.config.json').environmentId)\")"
```
Profile ID resolution (in order):
| Source | Used when |
|---|---|
| `$ARGUMENTS` contains `--profile-id <guid>` | Explicit override |
| `$ARGUMENTS` contains `--profile-name <name>` | Resolve via `GET /mobileofflineprofiles?$filter=name eq '<name>'&$select=mobileofflineprofileid` |
| `offline-profile.json` in cwd | Read top-level `profileId` field |
| Otherwise | `GET /mobileofflineprofiles` and present `AskUserQuestion` with the list (max 4 options) |
STOP if no profile can be resolved. Print: `Run /setup-offline-profile first, or pass --profile-id`.
> **`power.config.json` is intentionally NOT consulted here.** That file is owned by `npx power-apps init`. The profile ID lives in `offline-profile.json` only.
### Step 2 — Pick users/teams
`$ARGUMENTS` parsing:
| Flag | Effect |
|---|---|
| `--user <upn>` (repeatable) | Add specific user(s) by UPN (`user@domain.com`) |
| `--team <name>` (repeatable) | Add specific team(s) by name |
| `--me` | Add the current Dataverse user from `WhoAmI` / `systemusers(<UserId>)` — useful for solo dev demos |
| `--all-app-users` | Add every user with **System User** role in the current env (broad; intended for prod rollout — confirm at gate) |
| `--unassign-user <upn>` / `--unassign-team <name>` | Remove an existing membership rather than add |
If no flags passed, present `AskUserQuestion`:
> **Question**: "Who should receive this offline profile?"
>
> **Options** (max 4):
> - `Just me (the current user)` — equivalent to `--me`
> - `Pick specific users by UPN` — you reply with comma-separated emails in the next message
> - `Pick a team` — list env's teams and pick one
> - `All users with System User role` — equivalent to `--all-app-users`; broad scope, confirm at gate
For pick-users flow: after the choice, print:
> "Reply with comma-separated UPNs (e.g. `rm1@contoso.com, rm2@contoso.com`)"
Then read the next user message and parse.
### Step 3 — Discover existing memberships
For idempotency:
```bash
# Existing user memberships for this profile
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> GET \
"usermobileofflineprofilememberships?\$filter=_mobileofflineprofileid_value eq <profileId>&\$select=usermobileofflineprofilemembershipid,_systemuserid_value&\$expand=systemuserid_systemuser(\$select=domainname)"
# Existing team memberships for this profile
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> GET \
"teammobileofflineprofilememberships?\$filter=_mobileofflineprofileid_value eq <profileId>&\$select=teammobileofflineprofilemembershipid,_teamid_value&\$expand=teamid_team(\$select=name)"
```
Build the set of `already-bound` UPNs and team names.
For each candidate user/team from Step 2, look up their `systemuserid` / `teamid` (skip if already in `already-bound`):
```bash
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> GET \
"systemusers?\$filter=domainname eq '<upn>'&\$select=systemuserid,fullname,domainname&\$top=1"
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> GET \
"teams?\$filter=name eq '<team-name>' and teamtype eq 0&\$select=teamid,name&\$top=1"
```
(`teamtype eq 0` excludes Access Teams and Owner Teams — only Manage Teams get profile assignments.)
Construct three lists:
- `to_add` — resolved IDs to POST
- `to_remove` — resolved IDs to DELETE (from `--unassign-*` flags)
- `not_found` — UPNs/team-names that didn't resolve (warn)
- `already_bound` — skipped no-ops
### Step 4 — Confirm diff (single gate)
`AskUserQuestion`:
> **Question header**: `Confirm membership changes`
>
> **Question body**:
>
> ```
> Profile: <name> (<profileId>)
>
> Will ADD:
> - User: rahul@contoso.com (Rahul Bansal)
> - User: charanma@... (Charan Mahankali)
> - Team: Field Service RMs (12 members)
>
> Will REMOVE:
> (none)
>
> Already bound (skipping):
> - User: admin@... (no-op)
>
> Could not resolve:
> - someone@external.com — not in this env's system users
>
> Proceed?
> ```
>
> **Options**:
> - `Proceed`
> - `Cancel`
### Step 5 — POST memberships
For each in `to_add`, POST sequentially (parallel POSTs occasionally return 429):
**User membership:**
```bash
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> POST \
"usermobileofflineprofilememberships" \
--body '{
"MobileOfflineProfileId@odata.bind": "/mobileofflineprofiles(<profileId>)",
"SystemUserId@odata.bind": "/systemusers(<systemuserid>)"
}' \
--include-headers
```
Expected 204 with `OData-EntityId` → capture membership GUID.
**Team membership:**
```bash
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> POST \
"teammobileofflineprofilememberships" \
--body '{
"MobileOfflineProfileId@odata.bind": "/Guide the user to add a data source, connection, or API connector to a Canvas App via Power Apps Studio, then verify and continue. USE WHEN the user asks to add a data source, add a connection, add an API, add a connector, connect to SharePoint / Dataverse / SQL / Excel / OneDrive / Teams / Office 365, or any similar request to make new data available to the app. DO NOT USE WHEN the user is asking to list or describe existing data sources — call list_data_sources or list_apis directly instead.
Creates or edits a Power Apps Canvas App through the Canvas Authoring MCP coauthoring session. Handles new app generation, direct targeted edits, complex multi-screen changes, responsive layout, per-screen self-QA, and compile-error convergence. Trigger on requests to create, build, generate, modify, update, change, fix, or edit a Canvas App or .pa.yaml files.
Configure the Canvas Authoring MCP server for the current coauthoring session. USE WHEN "configure MCP", "set up MCP server", "MCP not working", "connect Canvas Apps MCP", "canvas-authoring not available", "MCP not configured", "set up canvas apps".
[DEPRECATED — use canvas-app instead] Generate a complete Power Apps canvas app.
>
Adds Azure DevOps connector to a Power Apps code app. Use when querying work items, creating bugs, managing pipelines, or making ADO API calls.
Use when adding a Power Platform connector to an Expo/React Native Power Apps mobile app and no dedicated mobile connector skill exists.
Use when adding an unspecified data source to an Expo/React Native Power Apps mobile app; routes to Dataverse, SharePoint, or another connector.