Skill v1.0.0
Trusted Publisher100/100version: "1.0.0" name: agents-sdk-typescript description: > Use when any code imports @microsoft/agents-hosting, @microsoft/agents-hosting-express, or related Agents SDK packages, or when the user is building, configuring, or asking questions about a Microsoft 365 Agents SDK agent in TypeScript. Trigger on questions about environment variables, connection configuration, AgentApplication patterns, OAuth sign-in flows, storage backends, cards, streaming, or local testing with the Agents Playground — even if no code exists yet and the user is planning or asking how to get started.
Overview
The Microsoft 365 Agents SDK builds multichannel agents for Teams, Copilot Studio, and web chat.
| Package | Purpose | |
|---|---|---|
@microsoft/agents-hosting | Core: AgentApplication, CloudAdapter, TurnContext, TurnState, storage | |
@microsoft/agents-hosting-express | startServer() convenience wrapper | |
@microsoft/agents-hosting-storage-blob | Azure Blob Storage backend | |
@microsoft/agents-hosting-storage-cosmos | CosmosDB backend | |
@microsoft/agents-hosting-dialogs | Dialog system |
Requires Node 18+. Use node --env-file .env (Node 20+) to load environment variables.
Azure Resources Required
Microsoft Entra App Registration
clientId— Application (client) IDclientSecret— Certificates & secretstenantId— Directory (tenant) ID
Azure Bot Resource
- Messaging endpoint:
https://<your-host>/api/messages - Microsoft App ID must match
clientId
Local dev: Use Bot Framework Emulator. No Azure Bot needed until deployment. Leave clientId blank to skip auth validation.
Environment Variables
Modern format (recommended)
Uses connections__ and connectionsMap__ prefixes. Double underscores (__) separate path segments; .settings. is stripped automatically.
Single connection (most common):
connections__serviceConnection__settings__clientId=<your-app-id>connections__serviceConnection__settings__clientSecret=<your-secret>connections__serviceConnection__settings__tenantId=<your-tenant-id>connectionsMap__0__connection=serviceConnectionconnectionsMap__0__serviceUrl=*
How `connectionsMap` works: Each entry maps a serviceUrl pattern to a named connection. The first matching entry wins.
serviceUrl=*— matches any service URL (use as the default/fallback)serviceUrlis treated as a regex for all other values
connectionsMap can be omitted when there is only one connection — the SDK defaults it to serviceUrl=*.
Multiple connections (e.g. different identities for different channels):
connections__mainConn__settings__clientId=<app-id-1>connections__mainConn__settings__clientSecret=<secret-1>connections__mainConn__settings__tenantId=<tenant-id>connections__teamsConn__settings__clientId=<app-id-2>connections__teamsConn__settings__clientSecret=<secret-2>connections__teamsConn__settings__tenantId=<tenant-id>connectionsMap__0__connection=teamsConnconnectionsMap__0__serviceUrl=https://smba.trafficmanager.net/.*connectionsMap__1__connection=mainConnconnectionsMap__1__serviceUrl=*
Optional audience field on a map entry restricts matching to activities whose JWT aud claim equals that value:
connectionsMap__0__connection=teamsConnconnectionsMap__0__serviceUrl=*connectionsMap__0__audience=<teams-app-id>
Available connection settings fields: clientId, clientSecret, tenantId, authority, certPemFile, certKeyFile, sendX5C, connectionName, scope
Legacy format — backwards compatibility only
Never use the legacy format for new agents. It exists solely for backwards compatibility with older BotFramework-based bots. Always use the modernconnections__format above.
clientId=<your-app-id>clientSecret=<your-secret>tenantId=<your-tenant-id>
Both startServer() and loadAuthConfigFromEnv() auto-detect the format. Leave clientId blank locally to skip auth.
OAuth authorization handler variables
These control the user sign-in flow configured via authorization: { [id]: { ... } }. The id is the key used in the authorization options (e.g. graph).
Modern format (recommended)
Uses the prefix AgentApplication__UserAuthorization__Handlers__<id>__Settings__<property>. Case-insensitive.
AzureBot handler (default — user OAuth flow via Azure Bot Service):
AgentApplication__UserAuthorization__Handlers__graph__Settings__azureBotOAuthConnectionName=GraphOAuthConnectionAgentApplication__UserAuthorization__Handlers__graph__Settings__title=Sign in with MicrosoftAgentApplication__UserAuthorization__Handlers__graph__Settings__text=Please sign in to continueAgentApplication__UserAuthorization__Handlers__graph__Settings__invalidSignInRetryMax=3AgentApplication__UserAuthorization__Handlers__graph__Settings__enableSso=false# OBO (on-behalf-of) — auto-exchange on routes using exchangeToken()AgentApplication__UserAuthorization__Handlers__graph__Settings__oboConnectionName=OBOConnectionAgentApplication__UserAuthorization__Handlers__graph__Settings__oboScopes=https://graph.microsoft.com/.default,Mail.Read# Custom error messagesAgentApplication__UserAuthorization__Handlers__graph__Settings__invalidSignInRetryMessage=That code was invalid, please try again.AgentApplication__UserAuthorization__Handlers__graph__Settings__invalidSignInRetryMessageFormat=Please enter the 6-digit code from the sign-in card.AgentApplication__UserAuthorization__Handlers__graph__Settings__invalidSignInRetryMaxExceededMessage=Too many failed attempts. Please try again later.
AgenticUserAuthorization handler (agent-to-agent, no user prompt):
AgentApplication__UserAuthorization__Handlers__myHandler__Settings__type=AgenticUserAuthorizationAgentApplication__UserAuthorization__Handlers__myHandler__Settings__scopes=https://graph.microsoft.com/.defaultAgentApplication__UserAuthorization__Handlers__myHandler__Settings__altBlueprintConnectionName=altConn
Legacy format (deprecated)
Do not use the legacy format for new agents. It exists solely for backwards compatibility. The SDK emits deprecation warnings when legacy variables are detected.
| Legacy variable | Modern equivalent | |
|---|---|---|
graph_connectionName | ...Handlers__graph__Settings__azureBotOAuthConnectionName | |
graph_connectionTitle | ...Handlers__graph__Settings__title | |
graph_connectionText | ...Handlers__graph__Settings__text | |
graph_maxAttempts | ...Handlers__graph__Settings__invalidSignInRetryMax | |
graph_enableSso | ...Handlers__graph__Settings__enableSso | |
graph_obo_connection | ...Handlers__graph__Settings__oboConnectionName | |
graph_obo_scopes | ...Handlers__graph__Settings__oboScopes | |
graph_messages_invalidCode | ...Handlers__graph__Settings__invalidSignInRetryMessage | |
graph_messages_invalidCodeFormat | ...Handlers__graph__Settings__invalidSignInRetryMessageFormat | |
graph_messages_maxAttemptsExceeded | ...Handlers__graph__Settings__invalidSignInRetryMaxExceededMessage | |
myHandler_type=agentic | ...Handlers__myHandler__Settings__type=AgenticUserAuthorization | |
myHandler_scopes | ...Handlers__myHandler__Settings__scopes | |
myHandler_altBlueprintConnectionName | ...Handlers__myHandler__Settings__altBlueprintConnectionName |
Priority when both formats are present: runtime options (code) > modern env vars > legacy env vars.
Quick Start
import { startServer } from '@microsoft/agents-hosting-express'import { AgentApplication, MemoryStorage, TurnContext, TurnState } from '@microsoft/agents-hosting'class MyAgent extends AgentApplication<TurnState> {constructor() {super({ storage: new MemoryStorage() })this.onConversationUpdate('membersAdded', async (ctx: TurnContext) => {await ctx.sendActivity('Hello! Send me a message.')})this.onActivity('message', async (ctx: TurnContext, state: TurnState) => {let counter: number = state.getValue('conversation.counter') || 0await ctx.sendActivity(`[${counter++}] You said: ${ctx.activity.text}`)state.setValue('conversation.counter', counter)})}}startServer(new MyAgent())
Run: node --env-file .env dist/index.js
Server Setup
Option A: startServer() (preferred)
startServer(agent) creates an Express app with:
express.json()+authorizeJWT()middlewarePOST /api/messagesroute- Listens on
PORTenv var (default 3978) - Returns the Express instance — add extra routes to the return value
import { startServer } from '@microsoft/agents-hosting-express'const server = startServer(agent)server.get('/health', (_req, res) => res.json({ ok: true }))
Option B: Manual Express
import express, { Response } from 'express'import { Request, CloudAdapter, authorizeJWT, loadAuthConfigFromEnv } from '@microsoft/agents-hosting'const authConfig = loadAuthConfigFromEnv()const adapter = new CloudAdapter(authConfig)const app = express()app.use(express.json())app.use(authorizeJWT(authConfig))app.post('/api/messages', async (req: Request, res: Response) => {await adapter.process(req, res, async (context) => await agent.run(context))})app.listen(process.env.PORT || 3978)
Proactive Messaging
Save a reference during a turn, then use adapter.continueConversation from any route. req.user comes from authorizeJWT middleware.
import { ConversationReference } from '@microsoft/agents-activity'// During a turn — save the referenceconst ref = ctx.activity.getConversationReference()conversationReferences[ref.conversation.id] = ref// In a proactive routeapp.get('/api/notify', async (req, res) => {for (const ref of Object.values(conversationReferences)) {await adapter.continueConversation(req.user!, ref, async (ctx) => {await ctx.sendActivity('Proactive message')})}res.json({ ok: true })})
Validating Your Configuration
1. Validate bot credentials (clientId / clientSecret / tenantId)
This tests that your Entra app registration credentials are correct and can authenticate with the Bot Framework. A successful response includes access_token; an error response includes error and error_description.
curl -s -X POST \"https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" \-d "grant_type=client_credentials\&client_id=$clientId\&client_secret=$clientSecret\&scope=https://api.botframework.com/.default" \| jq '{token_type, expires_in, error, error_description}'
Common errors:
AADSTS700016—clientIdnot found in tenant (wrong ID or wrong tenant)AADSTS7000215— invalidclientSecret(expired or incorrect)AADSTS90002—tenantIdnot found
2. Validate the agent is running and reachable
curl -s -o /dev/null -w "%{http_code}" \-X POST http://localhost:3978/api/messages \-H "Content-Type: application/json" \-d '{}'
401— agent is running; JWT auth rejected the empty request (expected — means auth is working)000or connection refused — agent is not running or wrong port200— agent is running with auth disabled (local dev with blankclientId)
3. Validate an OAuth connection name
OAuth connection names (set via AgentApplication__UserAuthorization__Handlers__graph__Settings__azureBotOAuthConnectionName) can only be tested end-to-end through a real sign-in flow. Use the Azure portal:
Azure Portal → Your Bot Resource → Settings → OAuth Connection Settings → [your connection] → Test Connection
This confirms the connection name matches, the OAuth app has the right scopes, and the redirect URI is configured correctly.
Local Testing with Agents Playground
The Agents Playground lets you test your agent locally without deploying to Azure or configuring a Bot resource. It acts as a mock connector service and channel client.
Install:
npm install -g agentsplayground
Recommended package.json scripts
Include these scripts when creating a new agent's package.json. The test script starts the agent and playground together in parallel — no separate terminals needed:
"scripts": {"prebuild": "npm ci","build": "tsc --build","prestart": "npm run build","prestart:anon": "npm run build","start:anon": "node ./dist/index.js","start": "node --env-file .env ./dist/index.js","test-tool": "agentsplayground -c emulator","test": "npm-run-all -p -r start:anon test-tool"}
npm start— builds and runs with.envcredentialsnpm test— builds, starts the agent without auth (start:anon), and launches the playground in parallel. Use this for quick local dev without needing an.envfile.-c emulator— uses the emulator channel (no auth required). Change tomsteams,webchat, etc. as needed.- Requires
npm-run-allas a dev dependency:npm install -D npm-run-all
With authentication (for testing OAuth/sign-in flows):
agentsplayground -c msteams \--client-id <your-app-id> \--client-secret <your-secret> \--tenant-id <your-tenant-id>
Channel options (-c): msteams, webchat, directline, emulator, agents
AgentApplication Patterns
Routing
this.onMessage('/cmd', handler) // exact commandthis.onActivity('message', handler) // all messagesthis.onConversationUpdate('membersAdded', handler)this.onActivity('invoke', handler)this.onActivity('message', fallback, [], RouteRank.Last) // fallback
TurnState — dot-notation keys auto-scoped to conversation/user/temp:
const count = state.getValue('conversation.counter') || 0state.setValue('conversation.counter', count + 1)state.deleteValue('conversation.counter')
Storage backends
| Backend | Use case | |
|---|---|---|
MemoryStorage | Local dev only — not persistent | |
BlobsStorage | Azure Blob — production | |
CosmosDbPartitionedStorage | CosmosDB — production |
Authorization (User Token Flow)
class MyAgent extends AgentApplication<TurnState> {constructor() {super({storage: new MemoryStorage(),authorization: {graph: {name: 'GraphOAuthConnection', // OAuth connection name in Azure Bot resourcetitle: 'Sign in with Microsoft',text: 'Please sign in to continue',}}})this.onSignInSuccess(async (ctx, state) => {const { token } = await this.authorization.getToken(ctx, 'graph')// use token to call external APIsawait ctx.sendActivity('Signed in!')})this.onSignInFailure(async (ctx, state, authId, err) => {await ctx.sendActivity(`Sign-in failed: ${err}`)})// Protect a route — SDK sends the OAuth card automatically if not signed inthis.onActivity('message', async (ctx, state) => {const { token } = await this.authorization.getToken(ctx, 'graph')// token is guaranteed here — route won't run until user is signed in}, ['graph'])// Sign out from all providersthis.onMessage('/logout', async (ctx, state) => {await this.authorization.signOut(ctx, state)await ctx.sendActivity('Signed out.')})}}
name can also be provided via environment variable AgentApplication__UserAuthorization__Handlers__graph__Settings__azureBotOAuthConnectionName (where graph is the handler key) and omitted from code.
OBO (on-behalf-of) — exchange user token for a downstream service token:
const { token } = await this.authorization.exchangeToken(ctx, 'graph', {scopes: ['https://graph.microsoft.com/.default']})// use token to call Graph or other downstream APIs
Agentic auth (agent-to-agent, no user prompt):
authorization: { agentic: { type: 'AgenticUserAuthorization' } }// env: AgentApplication__UserAuthorization__Handlers__agentic__Settings__type=AgenticUserAuthorization// env: AgentApplication__UserAuthorization__Handlers__agentic__Settings__scopes=https://graph.microsoft.com/.default
Cards
Read references/cards.md when working with Adaptive Cards, Hero Cards, Thumbnail Cards, or other card types.
Streaming
Read references/streaming.md when implementing streaming responses.
Common Mistakes
1. Wrong field name on ConversationReference
// WRONGconst ref: ConversationReference = { bot: { id: appId } }// CORRECTconst ref: ConversationReference = { agent: { id: appId } }
2. JwtPayload.aud is string | string[]
// WRONGconst appId = payload.aud// CORRECTconst appId = Array.isArray(payload.aud) ? payload.aud[0] : payload.aud
3. Adapter callbacks swallow exceptions
// WRONG — error is lostawait adapter.continueConversation(identity, ref, async (ctx) => {throw new Error('something failed') // swallowed})// CORRECT — capture and rethrowlet capturedError: Error | undefinedawait adapter.continueConversation(identity, ref, async (ctx) => {try {await doWork(ctx)} catch (err) {capturedError = err as Error}})if (capturedError) throw capturedError
4. startServer() returns Express — add routes to the return value
// WRONG — routes never registeredconst app = express()startServer(agent)app.get('/health', handler)// CORRECTconst server = startServer(agent)server.get('/health', handler)
Wrong method names — activity() and message() don't exist
AgentApplication uses on-prefixed method names. Common wrong guesses:
// ❌ wrong — these methods don't existapp.activity('message', handler)app.message(handler)// ✅ correctapp.onActivity('message', handler)app.onMessage('/help', handler)app.onConversationUpdate('membersAdded', handler)
Contributing
If you hit a problem this skill couldn't solve, found a workaround, or noticed something wrong or outdated, that's valuable — please help improve this skill for everyone.
Draft a suggested issue title and body based on the conversation, then ask the user to open it at: https://github.com/microsoft/agents/issues/new
A good issue includes:
- What the user was trying to do
- What went wrong (errors, unexpected behavior)
- What worked — including any workaround found during this conversation
- Relevant code or config snippets