Skip to main content
ClaudeWave
ES, Spanish
Skill1k estrellas del repoactualizado 5d ago

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.

Instalar en Claude Code
Copiar
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-profile
Después abre una sesión nueva de Claude Code; el skill carga automáticamente.

SKILL.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": "/