Skip to main content

Migrating from Teams SDK v1

Welcome, fellow agent developer! You've made it through a full major release of Teams SDK, and now you want to take the plunge into v2. In this guide, we'll walk you through everything you need to know, from migrating core features like message handlers and auth, to optional AI features like ActionPlanner. We'll also discuss how you can migrate features over incrementally via the botbuilder adapter.

Installing Teams SDK​

First, let's install Teams SDK into your project. Notably, this won't replace any existing installation of Teams SDK. When you've completed your migration, you can safely remove the @microsoft/teams-ai dependency from your package.json file.

npm install @microsoft/teams.apps

Migrate Application class​

First, migrate your Application class from v1 to the new App class.

-    import {
- ConfigurationServiceClientCredentialFactory,
- MemoryStorage,
- TurnContext,
- } from 'botbuilder';
- import { Application, TeamsAdapter } from '@microsoft/teams-ai';
- import * as restify from 'restify';
+ import { App } from '@microsoft/teams.apps';
+ import { LocalStorage } from '@microsoft/teams.common/storage';


- // Create adapter.
- const adapter = new TeamsAdapter(
- {},
- new ConfigurationServiceClientCredentialFactory({
- MicrosoftAppId: process.env.ENTRA_APP_CLIENT_ID,
- MicrosoftAppPassword: process.env.ENTRA_APP_CLIENT_SECRET,
- MicrosoftAppType: 'SingleTenant',
- MicrosoftAppTenantId: process.env.ENTRA_APP_TENANT_ID
- })
- );

- // Catch-all for errors.
- const onTurnErrorHandler = async (context: TurnContext, error: any) => {
- console.error(`\n [onTurnError] unhandled error: ${error}`);
- // Send a message to the user
- await context.sendActivity('The bot encountered an error or bug.');
- };

- // Set the onTurnError for the singleton CloudAdapter.
- adapter.onTurnError = onTurnErrorHandler;

- // Create HTTP server.
- const server = restify.createServer();
- server.use(restify.plugins.bodyParser());

- server.listen(process.env.port || process.env.PORT || 3978, () => {
- console.log(`\n${server.name} listening to ${server.url}`);
- });

- // Define storage and application
- const app = new Application<ApplicationTurnState>({
- storage: new MemoryStorage()
- });

- // Listen for incoming server requests.
- server.post('/api/messages', async (req, res) => {
- // Route received a request to adapter for processing
- await adapter.process(req, res, async (context) => {
- // Dispatch to application for routing
- await app.run(context);
- });
- });

* // Define app
* const app = new App({
* clientId: process.env.ENTRA_APP_CLIENT_ID!,
* clientSecret: process.env.ENTRA_APP_CLIENT_SECRET!,
* tenantId: process.env.ENTRA_TENANT_ID!,
* });

* // Optionally create local storage
* const storage = new LocalStorage();

* // Listen for errors
* app.event('error', async (client) => {
* console.error('Error event received:', client.error);
* if (client.activity) {
* await app.send(
* client.activity.conversation.id,
* 'An error occurred while processing your message.',
* );
* }
* });

* // App creates local server with route for /api/messages
* // To reuse your restify or other server,
* // create a custom `HttpPlugin`.
* (async () => {
* // starts the server
* await app.start();
* })();

Migrate activity handlers​

Both v1 and v2 are built atop incoming Activity requests, which trigger handlers in your code when specific type of activities are received. The syntax for how you register different types of Activity handlers differs [Dev] Section "activity-handlers-intro" not found in TypeScript documentation. Either mark the section explicitly as N/A for intentionally ignored, or fill in documentation. between the v1 and v2 versions of our SDK.

Message handlers​

// triggers when user sends "/hi" or "@bot /hi"
- app.message("/hi", async (context) => {
- await context.sendActivity("Hi!");
- });
+ app.message('/hi', async (client) => {
+ // SDK does not auto send typing indicators
+ await client.send({ type: 'typing' });
+ await client.send("Hi!");
+ });
// listen for ANY message to be received
- app.activity(
- ActivityTypes.Message,
- async (context) => {
- // echo back users request
- await context.sendActivity(
- `you said: ${context.activity.text}`
- );
- }
- );
+ app.on('message', async (client) => {
+ await client.send({ type: 'typing' });
+ await client.send(
+ `you said "${client.activity.text}"`
+ );
+ });

Task modules​

[Dev] Section "task-modules-note" not found in TypeScript documentation. Either mark the section explicitly as N/A for intentionally ignored, or fill in documentation.

-    app.taskModules.fetch('connect-account', async (context, state, data) => {
- const taskInfo: TaskModuleTaskInfo = {
- title: 'Connect your Microsoft 365 account',
- height: 'medium',
- width: 'medium',
- url: `https://${process.env.NEXT_PUBLIC_BOT_DOMAIN}/connections`,
- fallbackUrl: `https://${process.env.NEXT_PUBLIC_BOT_DOMAIN}/connections`,
- completionBotId: process.env.NEXT_PUBLIC_BOT_ID,
- };
- return taskInfo;
- });
- app.taskModules.submit('connect-account', async (context, state, data) => {
- console.log(
- `bot-app.ts taskModules.submit("connect-account"): data`,
- JSON.stringify(data, null, 4)
- );
- await context.sendActivity('You are all set! Now, how can I help you today?');
- return undefined;
- });
+ app.on('dialog.open', (client) => {
+ const dialogType = client.activity.value.data?.opendialogtype;
+ if (dialogType === 'some-type') {
+ return {
+ task: {
+ type: 'continue',
+ value: {
+ title: 'Dialog title',
+ height: 'medium',
+ width: 'medium',
+ url: `https://${process.env.YOUR_WEBSITE_DOMAIN}/some-path`,
+ fallbackUrl: `https://${process.env.YOUR_WEBSITE_DOMAIN}/fallback-path-for-web`,
+ completionBotId: process.env.ENTRA_APP_CLIENT_ID!,
+ },
+ },
+ };
+ }
+ });

- app.on('dialog.submit', async (client) => {
- const dialogType = client.activity.value.data?.submissiondialogtype;
- if (dialogType === 'some-type') {
- const { data } = client.activity.value;
- await client.send(JSON.stringify(data));
- }
- return undefined;
- });

Learn more in the Dialogs guide.

Adaptive cards​

In Teams SDK v2, cards have much more rich type validation than existed in v1. However, assuming your cards were valid, it should be easy to migrate to v2.

-    app.message('/card', async (context: TurnContext) => {
- const card = CardFactory.adaptiveCard({
- $schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
- version: '1.5',
- type: 'AdaptiveCard',
- body: [
- {
- type: 'TextBlock',
- text: 'Hello, world!',
- wrap: true,
- isSubtle: false,
- },
- ],
- msteams: {
- width: 'Full',
- },
- });
- await context.sendActivity({
- attachments: [card],
- });
- });
+ app.message('/card', async (client) => {
+ await client.send({
+ $schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
+ version: '1.5',
+ type: 'AdaptiveCard',
+ body: [
+ {
+ type: 'TextBlock',
+ text: 'Hello, world!',
+ wrap: true,
+ isSubtle: false,
+ },
+ ],
+ msteams: {
+ width: 'Full',
+ },
+ });
+ });

Learn more in the Adaptive Cards guide.

Authentication​

Most agents feature authentication for user identification, interacting with APIs, etc. Whether your Teams SDK app used Entra SSO or custom OAuth, porting to v2 should be simple.

-    const storage = new MemoryStorage();
- const app = new Application({
- storage: new MemoryStorage(),
- authentication: {
- autoSignIn: (context) => {
- const activity = context.activity;
- // No auth when user wants to sign in
- if (activity.text === '/signout') {
- return Promise.resolve(false);
- }
- // No auth for "/help"
- if (activity.text === '/help') {
- return Promise.resolve(false);
- }
- // Manually sign in (for illustrative purposes)
- if (activity.text === '/signin') {
- return Promise.resolve(false);
- }
- // For all other messages, require sign in
- return Promise.resolve(true);
- },
- settings: {
- graph: {
- connectionName: process.env.OAUTH_CONNECTION_NAME!,
- title: 'Sign in',
- text: 'Please sign in to use the bot.',
- endOnInvalidMessage: true,
- tokenExchangeUri: process.env.TOKEN_EXCHANGE_URI!,
- enableSso: true,
- },
- },
- },
- });
-
- app.message('/signout', async (context, state) => {
- await app.authentication.signOutUser(context, state);
- await context.sendActivity(`You have signed out`);
- });
-
- app.message('/help', async (context, state) => {
- await context.sendActivity(`your help text`);
- });
-
- app.authentication.get('graph').onUserSignInSuccess(async (context, state) => {
- await context.sendActivity('Successfully logged in');
- await context.sendActivity(`Token string length: ${state.temp.authTokens['graph']!.length}`);
- });
+ const app = new App({
+ oauth: {
+ defaultConnectionName: 'graph',
+ },
+ logger: new ConsoleLogger('@tests/auth', { level: 'debug' }),
+ });
+
+ app.message('/signout', async (client) => {
+ if (!client.isSignedIn) return;
+ await client.signout();
+ await client.send('you have been signed out!');
+ });
+
+ app.message('/help', async (client) => {
+ await client.send('your help text');
+ });
+
+ app.on('message', async (client) => {
+ if (!client.isSignedIn) {
+ await client.signin({
+ oauthCardText: 'Sign in to your account',
+ signInButtonText: 'Sign in',
+ });
+ return;
+ }
+ const me = await client.userGraph.me.get();
+ log.info(`user "${me.displayName}" already signed in!`);
+ });
+
+ app.event('signin', async (client) => {
+ const me = await client.userGraph.me.get();
+ await client.send(`user "${me.displayName}" signed in.`);
+ await client.send(`Token string length: ${client.token.token.length}`);
+ });

AI​

Action planner​

When we created Teams SDK, LLM's didn't natively support tool calling or orchestration. A lot has changed since then, which is why we decided to deprecate ActionPlanner from Teams SDK, and replace it with something a bit more lightweight. Notably, Teams SDK had two similar concepts: functions and actions. In Teams SDK, these are consolidated into functions.

-    // Create AI components
- const model = new OpenAIModel({
- apiKey: process.env.OPENAI_KEY!,
- defaultModel: 'gpt-4o',
- logRequests: true,
- });
-
- const prompts = new PromptManager({
- promptsFolder: path.join(__dirname, '../src/prompts'),
- });
-
- // Define a prompt function for getting the current status of the lights
- prompts.addFunction('getLightStatus', async (context, memory) => {
- return memory.getValue('conversation.lightsOn') ? 'on' : 'off';
- });
-
- const planner = new ActionPlanner({
- model,
- prompts,
- defaultPrompt: 'tools',
- });
-
- // Define storage and application
- const storage = new MemoryStorage();
- const app = new Application<ApplicationTurnState>({
- storage,
- ai: {
- planner,
- },
- });
-
- // Register action handlers
- app.ai.action('ToggleLights', async (context, state) => {
- state.conversation.lightsOn = !state.conversation.lightsOn;
- const lightStatusText = state.conversation.lightsOn ? 'on' : 'off';
- await context.sendActivity(`[lights ${lightStatusText}]`);
- return `the lights are now ${lightStatusText}$`;
- });
-
- app.ai.action('Pause', async (context, state, parameters: PauseParameters) => {
- await context.sendActivity(`[pausing for ${parameters.time / 1000} seconds]`);
- await new Promise((resolve) => setTimeout(resolve, parameters.time));
- return `done pausing`;
- });
+ const storage = new LocalStorage<IStorageState>();
+ const app = new App();
+
+ app.on('message', async (client) => {
+ let state = storage.get(client.activity.from.id);
+
+ if (!state) {
+ state = {
+ status: false,
+ messages: [],
+ };
+ storage.set(client.activity.from.id, state);
+ }
+
+ const prompt = new ChatPrompt({
+ messages: state.messages,
+ instructions: `The assistant can turn a light on or off. The lights are currently off.`,
+ model: new OpenAIChatModel({
+ model: 'gpt-4o-mini',
+ apiKey: process.env.OPENAI_API_KEY,
+ }),
+ })
+ .function('get_light_status', 'get the current light status', () => {
+ return state.status;
+ })
+ .function('toggle_lights', 'toggles the lights on/off', () => {
+ state.status = !state.status;
+ storage.set(client.activity.from.id, state);
+ })
+ .function(
+ 'pause',
+ 'delays for a period of time',
+ {
+ type: 'object',
+ properties: {
+ time: {
+ type: 'number',
+ description: 'the amount of time to delay in milliseconds',
+ },
+ },
+ required: ['time'],
+ },
+ async ({ time }: { time: number }) => {
+ await new Promise((resolve) => setTimeout(resolve, time));
+ }
+ );
+
+ await prompt.send(client.activity.text, {
+ onChunk: (chunk) => {
+ client.stream.emit(new MessageActivity(chunk));
+ },
+ });
+ });

Feedback​

If you supported feedback for AI generated messages, migrating is simple.

-    export const app = new Application({
- ai: {
- // opts into feedback loop
- enable_feedback_loop: true,
- },
- });
-
- // Reply with message including feedback buttons
- app.activity(ActivityTypes.Message, async (context) => {
- await context.sendActivity({
- type: ActivityTypes.Message,
- text: `Hey, give me feedback!`,
- channelData: {
- feedbackLoop: {
- type: 'custom',
- },
- },
- });
- });
-
- // Handle feedback submit
- app.feedbackLoop(async (context, state, feedbackLoopData) => {
- // custom logic here...
- });
+ // Reply with message including feedback buttons
+ app.on('message', async (client) => {
+ await client.send(
+ new MessageActivity('Hey, give me feedback!')
+ .addAiGenerated() // AI generated label
+ .addFeedback() // Feedback buttons
+ );
+ });
+
+ // Listen for feedback submissions
+ app.on('message.submit.feedback', async ({ activity, log }) => {
+ // custom logic here...
+ });

You can learn more about feedback in Teams SDK in the Feedback guide.

Incrementally migrating code via botbuilder plugin​

info

Comparison code coming soon!

If you aren't ready to migrate all of your code, you can run your existing Teams SDK code in parallel with Teams SDK. Learn more here.