<< All versions

Skill v1.0.0

currentTrusted Publisher100/100
microsoft/managed-apps/add-office365-users
──Details
PublishedOctober 17, 2026 at 08:14 PM
Content Hashsha256:0ff6b056a12db177...
Git SHA
──Files
Files (1 file, 5.3 KB)
SKILL.md5.3 KBactive
SKILL.md · 173 lines · 5.3 KB

version: "1.0.0" name: add-office365-users description: Adds Office 365 Users by delegating to /add-data-source with api-id=shared_office365users and action mode. Use for user profiles, managers, direct reports, people search, and profile photos. user-invocable: true allowed-tools: Read, AskUserQuestion, Skill model: sonnet


📋 Shared Instructions: [shared-instructions.md](${CLAUDE_PLUGIN_ROOT}/shared/shared-instructions.md) — Cross-cutting concerns.

Add Office 365 Users (Wrapper)

This skill is a thin wrapper. Use /add-data-source as the single implementation path.

Delegation contract

Invoke /add-data-source with:

  • api-id: shared_office365users
  • mode: action

Office 365 Users: Method Selection Guide

After the connector is added, import the generated service:

typescript
import { Office365UsersService } from '../../generated/services/Office365UsersService'

Use the V2 methods for profiles and reporting relationships:

ScenarioMethodResult
Current signed-in userMyProfile_V2($select?)One GraphUser_V1
Specific user by ID or UPNUserProfile_V2(id, $select?)One GraphUser_V1
User's managerManager_V2(id, $select?)One GraphUser_V1
Direct reportsDirectReports_V2(id, $select?, $top?)Profiles in result.data.value
Search peopleSearchUserV2(searchTerm, top?, ...)Search response in result.data.value
Profile photoUserPhotoMetadata(userId), then UserPhoto_V2(id)Metadata, then binary image data

DirectReports_V2 requires the target manager's Microsoft Entra directory ID or user principal name (UPN). For the signed-in user, call MyProfile_V2('id,userPrincipalName'), then pass the returned id or fall back to userPrincipalName. For a known person, pass their directory ID or UPN directly.

Profile and direct-report pattern

Request only the fields the UI needs:

typescript
const fields = [
'id',
'displayName',
'jobTitle',
'department',
'mail',
'userPrincipalName',
'officeLocation',
].join(',')
const myProfile = await Office365UsersService.MyProfile_V2(
'id,userPrincipalName',
)
if (!myProfile.success) {
throw new Error(myProfile.error?.message ?? 'Failed to resolve current user')
}
const managerId =
myProfile.data?.id ?? myProfile.data?.userPrincipalName
if (!managerId) {
throw new Error('Current user profile did not include an ID or UPN')
}
const result = await Office365UsersService.DirectReports_V2(
managerId,
fields,
)
if (!result.success) {
throw new Error(result.error?.message ?? 'Failed to load direct reports')
}
const people = result.data?.value ?? []

An empty value array is valid and means the user has no direct reports.

Profile photos: binary response and CSP-safe rendering

Call UserPhotoMetadata before downloading the photo. HasPhoto: false is a valid no-photo state; render initials or another accessible fallback.

Image responses are a special runtime contract: the generated UserPhoto_V2 signature may declare IOperationResult<string>, but the Managed Apps SDK decodes an image/* response into a Uint8Array. Do not interpolate that byte array directly into a base64 URL, and do not use a blob: URL—the deployed App Player content security policy may block blob: under img-src.

Convert the bytes to a data: URL:

typescript
function bytesToBase64(bytes: Uint8Array) {
const chunkSize = 0x8000
let binary = ''
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
binary += String.fromCharCode(
...bytes.subarray(offset, Math.min(offset + chunkSize, bytes.length)),
)
}
return btoa(binary)
}
function toImageDataUrl(value: unknown, contentType = 'image/jpeg') {
if (value instanceof Uint8Array) {
return `data:${contentType};base64,${bytesToBase64(value)}`
}
if (value instanceof ArrayBuffer) {
return `data:${contentType};base64,${bytesToBase64(new Uint8Array(value))}`
}
if (typeof value === 'string') {
if (/^data:image\/[a-z0-9.+-]+;base64,/i.test(value)) {
return value
}
if (/^https?:/i.test(value)) {
throw new Error(
'Profile photo returned an HTTP URL instead of binary image data.',
)
}
return `data:${contentType};base64,${value}`
}
throw new Error('The profile photo response used an unsupported format.')
}

UserPhoto_V2 has a binary response contract and does not return an external image URL. An HTTP value indicates an unexpected response shape, so the helper rejects it rather than passing a potentially CSP-blocked or authenticated URL to <img src>.

Complete photo flow:

typescript
const metadata = await Office365UsersService.UserPhotoMetadata(userId)
if (!metadata.success) {
throw new Error(metadata.error?.message ?? 'Failed to check profile photo')
}
if (!metadata.data?.HasPhoto) {
return undefined
}
const photo = await Office365UsersService.UserPhoto_V2(userId)
if (!photo.success) {
throw new Error(photo.error?.message ?? 'Failed to load profile photo')
}
return photo.data
? toImageDataUrl(photo.data, metadata.data.ContentType)
: undefined

Always fetch photos through the generated connector service. Do not call Microsoft Graph directly and do not assign the authenticated connector runtime URL to <img src>.

All versions