Skip to main content

graph-integration

Use this skill when the user is calling Microsoft Graph from a Microsoft Teams app on Teams SDK 2.1 — `app.graph.call(endpoints…)` as the app, a `GraphClient` built from a user's OAuth token, `@microsoft/teams.graph-endpoints` parameters, beta endpoints, paging, permissions. Triggers on "Microsoft Graph Teams", "graph-endpoints", "app.graph", "list calendar events bot", "send mail Teams bot", "graph permissions".

Source facts

Repository
Agents-Store/opencode-plugins
Last source activity
October 3, 2026 at 23:16
Detected SKILL.md language
English
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
graph-integration
description
Use this skill when the user is calling Microsoft Graph from a Microsoft Teams app on Teams SDK 2.1 — `app.graph.call(endpoints…)` as the app, a `GraphClient` built from a user's OAuth token, `@microsoft/teams.graph-endpoints` parameters, beta endpoints, paging, permissions. Triggers on "Microsoft Graph Teams", "graph-endpoints", "app.graph", "list calendar events bot", "send mail Teams bot", "graph permissions".
# Microsoft Graph integration Graph is split into three packages. There is no fluent `graph.users.list()` object on the API client. | Package | Role | |---|---| | `@microsoft/teams.graph` | A small client: `new Client(options).call(endpoint, params)` | | `@microsoft/teams.graph-endpoints` | Typed request builders for the v1.0 API (tree-shakable; install it explicitly) | | `@microsoft/teams.graph-endpoints-beta` | The same for the beta API; can be mixed with v1.0 calls | ```bash npm install @microsoft/teams.graph @microsoft/teams.graph-endpoints ``` The picture to keep in mind: **an endpoint builder describes the request, a client signs and sends it.** Which client you use decides whose token travels. ## 1. As the app (application permissions) `app.graph` and, inside a handler, `appGraph` use the bot's own identity — the same `CLIENT_ID`/`CLIENT_SECRET`/`TENANT_ID`, no extra credentials: ```ts import * as endpoints from '@microsoft/teams.graph-endpoints'; app.on('message', async ({ send, appGraph }) => { const users = await appGraph.call(endpoints.users.list, { $top: 5, $select: ['id', 'displayName'], }); await send(`First users: ${(users.value ?? []).map((u) => u.displayName).join(', ')}`); }); ``` All arguments — path variables, query options, request body — go into one object after the endpoint. Application permissions (`User.Read.All`, `Team.ReadBasic.All`, …) need admin consent in the tenant. ## 2. As the user (delegated permissions) Get the user's token from an OAuth flow (`authentication`), then build a client around it. The client is bound to the connection that owns the token: ```ts import { Client as GraphClient } from '@microsoft/teams.graph'; import * as endpoints from '@microsoft/teams.graph-endpoints'; const graph = app.addOAuthFlow('graph'); app.on('message', async (ctx) => { const token = await graph.signIn(ctx); if (!token) return; // an OAuth card was sent const client = new GraphClient({ token: () => token }, { baseUrlRoot: app.graphBaseUrl }); const events = await client.call(endpoints.me.events.list, { $top: 10, $orderby: ['start/dateTime'], $select: ['subject', 'start'], }); await ctx.send(`Next: ${(events.value ?? []).map((e) => e.subject).join(', ')}`); }); ``` `token` can be a function so a refreshed token is picked up on each request. `baseUrlRoot: app.graphBaseUrl` routes sovereign-cloud apps to the right Graph host (it is `undefined` for the public cloud, which the client defaults to). The SDK 2.0 object `userGraph` on the context still compiles, deprecated; new code builds the client from the flow. ## 3. Path variables, query options and bodies ```ts import * as endpoints from '@microsoft/teams.graph-endpoints'; async function demo(client: import('@microsoft/teams.graph').Client, userId: string) { const user = await client.call(endpoints.users.get, { 'user-id': userId, $select: ['id', 'displayName', 'mail'] }); const teams = await client.call(endpoints.me.joinedTeams.list); void user; void teams; } void demo; ``` - Path placeholders keep their Graph names: `'user-id'`, `'team-id'`, `'channel-id'`. - `$select`, `$orderby`, `$expand` take arrays; `$top` takes a number; `$search` queries need `ConsistencyLevel: 'eventual'` as a parameter. - Request bodies are strictly typed. Nested complex types carry an `'@odata.type'` discriminator: ```ts import { Client as GraphClient } from '@microsoft/teams.graph'; import * as endpoints from '@microsoft/teams.graph-endpoints'; async function createStandup(client: GraphClient) { await client.call(endpoints.me.events.create, { '@odata.type': 'microsoft.graph.event', subject: 'Stand-up', start: { '@odata.type': 'microsoft.graph.dateTimeTimeZone', dateTime: '2026-05-19T09:00:00', timeZone: 'UTC' }, end: { '@odata.type': 'microsoft.graph.dateTimeTimeZone', dateTime: '2026-05-19T09:15:00', timeZone: 'UTC' }, }); } void createStandup; ``` If the compiler rejects a body, read the expected type — it lists the required discriminators. `sendMail` takes its body under a capitalised `Message` key. ## 4. Beta endpoints and custom requests ```ts import * as endpointsBeta from '@microsoft/teams.graph-endpoints-beta'; import type { EndpointRequest } from '@microsoft/teams.graph'; async function betaMe() { return app.graph.call(endpointsBeta.me.get); // GET /beta/me } const getMyDisplayName = (): EndpointRequest<{ displayName: string }> => ({ ver: 'beta', method: 'get', path: '/me', paramDefs: { query: ['$select'] }, params: { $select: ['displayName'] }, }); async function customCall() { const { displayName } = await app.graph.call(getMyDisplayName); return displayName; } void betaMe; void customCall; ``` A hand-written `EndpointRequest` gives a narrower return type, or reaches an API the endpoints packages do not have yet. ## 5. Paging Collections return `@odata.nextLink`. Follow it with the client's own HTTP client, which already carries the token and base URL: ```ts import { Client as GraphClient } from '@microsoft/teams.graph'; import * as endpoints from '@microsoft/teams.graph-endpoints'; async function allMessages(client: GraphClient) { let page = await client.call(endpoints.me.messages.list, { $top: 50 }); const all = [...(page.value ?? [])]; while (page['@odata.nextLink']) { const res = await client.http.get<typeof page>(page['@odata.nextLink']); page = res.data; all.push(...(page.value ?? [])); } return all; } void allMessages; ``` ## 6. Throttling Graph throttles per app and tenant. On `429`, wait for the `Retry-After` period and retry with a bounded number of attempts; do not loop in the handler — a handler that runs long should answer first and finish with `app.send`. ## 7. Permissions and consent - Delegated permissions are consented by the user at sign-in, or by an admin in advance; the Graph scopes belong on the OAuth connection too. - Application permissions always need admin consent; sign-in does not grant them. - Resource-specific consent (RSC) for team or chat data is managed with `teams app rsc add|set` (`cli-recipes`). - The `graph` CLI template shows the end-to-end sign-in; its code uses the 2.0 style, the shape here is the 2.1 one. ## Common pitfalls - **`403 Forbidden`** — wrong identity (app token on a `me` call), a missing scope, or no admin consent. Decode the token at jwt.ms and compare `scp` (delegated) or `roles` (application). - **`401` after a long session** — the user's token expired; call `graph.signIn(ctx)` again. - **A type error on a request body** — a nested object lacks its `'@odata.type'`, or the key is cased differently from Graph (`Message` for `sendMail`). - **An empty `value`** — the collection is paged; follow `@odata.nextLink`. - **`InvalidAuthenticationToken`** — the client holds the app token where a delegated one is required, or the reverse.
View on GitHub