I-edit

Tutorial: Build an agentic web app in Azure App Service with LangGraph or Foundry Agent Service (Node.js)

This tutorial demonstrates how to add agentic capability to an existing data-driven Express.js CRUD application. It does this using two different approaches: LangGraph and Foundry Agent Service.

If your web application already has useful features, like shopping, hotel booking, or data management, it's relatively straightforward to add agent functionality to your web application by wrapping those functionalities in a plugin (for LangGraph) or as an OpenAPI endpoint (for Foundry Agent Service). In this tutorial, you start with a simple to-do list app. By the end, you'll be able to create, update, and manage tasks with an agent in an App Service app.

Both LangGraph and Foundry Agent Service enable you to build agentic web applications with AI-driven capabilities. LangGraph is similar to Microsoft Semantic Kernel and is an SDK, but Semantic Kernel doesn't support JavaScript currently. The following table shows some of the considerations and trade-offs:

Consideration LangGraph Foundry Agent Service
Performance Fast (runs locally) Slower (managed, remote service)
Development Full code, maximum control Low code, rapid integration
Testing Manual/unit tests in code Built-in playground for quick testing
Scalability App-managed Azure-managed, autoscaled
Security guardrails Custom implementation required Built-in content safety and moderation
Identity Custom implementation required Built-in agent ID and authentication
Enterprise Custom integration required Built-in Microsoft 365/Teams deployment and Microsoft 365 integrated tool calls.

In the deployed app, App Service authentication requires Microsoft Entra sign-in for both the browser UI and the APIs. LangGraph runs inside App Service and calls the task service directly. Foundry Agent Service runs remotely and calls the protected task API through its OpenAPI tool.

In this tutorial, you learn how to:

  • Convert existing app functionality into a plugin for LangGraph.
  • Add the plugin to a LangGraph agent and use it in a web app.
  • Convert existing app functionality into an OpenAPI endpoint for Foundry Agent Service.
  • Call a Foundry agent in a web app.
  • Assign the required permissions for managed identity connectivity.
  • Protect an App Service web app and its APIs with Microsoft Entra ID.
  • Configure a Foundry OpenAPI tool to call protected App Service APIs with managed identity.

Prerequisites

Open the sample with Codespaces

The easiest way to get started is by using GitHub Codespaces, which provides a complete development environment with all required tools preinstalled.

  1. Navigate to the GitHub repository at https://github.com/Azure-Samples/app-service-agentic-langgraph-foundry-node.

  2. Select the Code button, select the Codespaces tab, and select Create codespace on main.

  3. Wait a few moments for your Codespace to initialize. When ready, you'll see a fully configured development environment in your browser.

  4. Run the application locally:

    npm install
    npm run build
    npm start
    
  5. When you see Your application running on port 3000 is available, select Open in Browser and add a few tasks.

    The agents aren't fully configured so they don't work yet. You'll configure them later.

Review the agent code

Both approaches use the same implementation pattern, where the agent is initialized on application start, and responds to user messages by POST requests.

The LangGraphTaskAgent is initialized in the constructor in src/agents/LangGraphTaskAgent.ts. The initialization code does the following:

    constructor(taskService: TaskService) {
        this.taskService = taskService;
        this.memory = new MemorySaver();
        try {
            const endpoint = process.env.AZURE_OPENAI_ENDPOINT;
            const deploymentName = process.env.AZURE_OPENAI_DEPLOYMENT_NAME;

            if (!endpoint || !deploymentName) {
                console.warn('Azure OpenAI configuration missing for LangGraph agent');
                return;
            }
            // Initialize Azure OpenAI client
            const credential = new DefaultAzureCredential();
            const azureADTokenProvider = getBearerTokenProvider(credential, "https://cognitiveservices.azure.com/.default");
            
            this.llm = new AzureChatOpenAI({
                azureOpenAIEndpoint: endpoint,
                azureOpenAIApiDeploymentName: deploymentName,
                azureADTokenProvider: azureADTokenProvider,
                azureOpenAIApiVersion: "2024-10-21"
            });
            // Define tools directly in the array
            const tools = [
                tool(
                    async ({ title, isComplete = false }) => {
                        const task = await this.taskService.addTask(title, isComplete);
                        return `Task created successfully: "${task.title}" (ID: ${task.id})`;
                    },
                    {
                        name: 'createTask',
                        description: 'Create a new task',
                        schema: z.object({
                            title: z.string(),
                            isComplete: z.boolean().optional()
                        }) as any
                    }
                ),
                tool(
                    async () => {
                        const tasks = await this.taskService.getAllTasks();
                        if (tasks.length === 0) {
                            return 'No tasks found.';
                        }
                        return `Found ${tasks.length} tasks:\n` + 
                               tasks.map(t => `- ${t.id}: ${t.title} (${t.isComplete ? 'Complete' : 'Incomplete'})`).join('\n');
                    },
                    {
                        name: 'getTasks',
                        description: 'Get all tasks',
                        schema: z.object({}) as any
                    }
                ),
                tool(
                    async ({ id }) => {
                        const task = await this.taskService.getTaskById(id);
                        if (!task) {
                            return `Task with ID ${id} not found.`;
                        }
                        return `Task ${task.id}: "${task.title}" - Status: ${task.isComplete ? 'Complete' : 'Incomplete'}`;
                    },
                    {
                        name: 'getTask',
                        description: 'Get a specific task by ID',
                        schema: z.object({
                            id: z.number()
                        }) as any
                    }
                ),
                tool(
                    async ({ id, title, isComplete }) => {
                        const updated = await this.taskService.updateTask(id, title, isComplete);
                        if (!updated) {
                            return `Task with ID ${id} not found.`;
                        }
                        return `Task ${id} updated successfully.`;
                    },
                    {
                        name: 'updateTask',
                        description: 'Update an existing task',
                        schema: z.object({
                            id: z.number(),
                            title: z.string().optional(),
                            isComplete: z.boolean().optional()
                        }) as any
                    }
                ),
                tool(
                    async ({ id }) => {
                        const deleted = await this.taskService.deleteTask(id);
                        if (!deleted) {
                            return `Task with ID ${id} not found.`;
                        }
                        return `Task ${id} deleted successfully.`;
                    },
                    {
                        name: 'deleteTask',
                        description: 'Delete a task',
                        schema: z.object({
                            id: z.number()
                        }) as any
                    }
                )
            ];

            // Create the ReAct agent with memory
            this.agent = createReactAgent({
                llm: this.llm,
                tools,
                checkpointSaver: this.memory,
                stateModifier: `You are an AI assistant that manages tasks using CRUD operations.
                
You have access to tools for creating, reading, updating, and deleting tasks.
Always use the appropriate tool for any task management request.
Be helpful and provide clear responses about the actions you take.

If you need more information to complete a request, ask the user for it.`
            });
        } catch (error) {
            console.error('Error initializing LangGraph agent:', error);
        }
    }

The deployed sample is protected by App Service authentication and uses one server-selected LangGraph thread. When you process user messages, the agent invokes invoke() with the user's message and the server-managed thread ID:

private readonly conversationThreadId = 'authenticated-conversation';

const result = await this.agent.invoke(
    {
        messages: [
            { role: 'user', content: message }
        ]
    },
    {
        configurable: {
            thread_id: this.conversationThreadId
        }
    }
);

Deploy the sample application

The sample repository contains an Azure Developer CLI (AZD) template, which creates an App Service app and deploys your sample application. The template enables a system-assigned managed identity for outbound Azure AI calls and configures App Service authentication with Microsoft Entra ID. For more information about the underlying authentication configuration, see Secure OpenAPI endpoints for Foundry Agent Service.

  1. In the terminal, sign in to Azure by using Azure Developer CLI:

    azd auth login
    

    Follow the instructions to complete the authentication process.

  2. Deploy the Azure App Service app by using the AZD template:

    azd up
    
  3. When prompted, give the following answers:

    Question Answer
    Enter a new environment name: Type a unique name.
    Select an Azure Subscription to use: Select the subscription.
    Pick a resource group to use: Select Create a new resource group.
    Select a location to create the resource group in: Select Sweden Central.
    Enter a name for the new resource group: Type Enter.
  4. In the AZD output, find the URL of your app. Also copy the Foundry OpenAPI managed identity audience value for later. The output looks like this:

     Deploying services (azd deploy)
    
       (✓) Done: Deploying service web
       - Endpoint: <URL>
    
     Foundry OpenAPI managed identity audience:
     api://<generated-client-id>
     
  5. Open the App Service endpoint from the AZD output.

  6. When Microsoft prompts you, sign in by using an account in the deployment tenant, and verify that the task list loads.

  7. In the same authenticated browser, open the autogenerated OpenAPI schema at https://<app-name>.azurewebsites.net/api/schema.

  8. Copy or save the generated OpenAPI schema. You use it in the Foundry Agent Service pivot.

    Note

    App Service authentication returns an HTTP 302 redirect for unauthenticated browser requests. This sample contains both a browser UI and APIs, so the redirect provides a usable sign-in experience. API-only apps commonly use HTTP 401 instead.

    You now have an authenticated App Service app. Its system-assigned managed identity is used for outbound Foundry calls. A separate user-assigned managed identity provides secretless credentials for App Service authentication.

Create and configure the Microsoft Foundry resource

  1. In the Foundry portal, create a project.

  2. Deploy a model of your choice (see Microsoft Foundry Quickstart: Create resources).

  3. From top of the model playground, copy the model name.

  4. On the home page, copy the Azure OpenAI endpoint for later.

Assign required permissions

  1. In the Foundry portal, select Manage in the top menu.

  2. In Project details, select the Parent resource for your project, and then select Open in Azure portal.

    From the Azure portal, you can assign role-based access for the resource.

  3. Add the following role for both the App Service app's managed identity and the user you use with az login:

    Target resource Required role Needed for
    Foundry Cognitive Services OpenAI User The chat completion service in Microsoft Agent Framework.

    For instructions, see Assign Azure roles using the Azure portal.

Configure connection variables in your sample application

  1. Open .env. Using the values you copied earlier from the Foundry portal, configure the following variables:

    Variable Description
    AZURE_OPENAI_ENDPOINT Azure OpenAI endpoint (copied from the Foundry portal home page).
    AZURE_OPENAI_DEPLOYMENT_NAME Model name in the deployment (copied from the model playground in the new Foundry portal).

    Note

    To keep the tutorial simple, you'll use these variables in .env instead of overwriting them with app settings in App Service.

    Note

    To keep the tutorial simple, you'll use these variables in .env instead of overwriting them with app settings in App Service.

    The values in .env configure the app's outbound connection to Foundry. AZURE_AI_FOUNDRY_ACCOUNT_CLIENT_ID configures the separate inbound Foundry-to-App-Service OpenAPI connection and is stored in the AZD environment.

App Service authentication runs in Azure, not in the local Express process, so the local testing workflow remains unchanged.

  1. Sign in to Azure with the Azure CLI:

    az login
    

    This allows the Azure Identity client library in the sample code to receive an authentication token for the logged in user. Remember that you added the required role for this user earlier.

  2. Run the application locally:

    npm run build
    npm start
    
  3. When you see Your application running on port 3000 is available, select Open in Browser.

  4. Validate both pivots separately:

    • LangGraph: Select LangGraph Agent, and ask the agent to create a task. LangGraph calls the in-process task tool.
    • Foundry Agent Service: Select Foundry Agent, and ask the agent to create a task. The remote Foundry agent calls the deployed, protected /api/tasks endpoint with managed identity.

    The task that the Foundry agent creates appears in the deployed App Service instance, not the local in-memory database. The Foundry OpenAPI tool always uses the server URL embedded in the OpenAPI schema.

  5. Back in the GitHub codespace, deploy your app changes.

    azd up
    
  6. Navigate to the deployed application, sign in, and test both pivots. Create and list tasks with the LangGraph Agent, and then create and list tasks with the Foundry Agent. Verify that both pivots update the task list.

Frequently asked questions

How do I add retrieval augmented generation (RAG) to the Foundry agent?

This guidance applies to the Foundry Agent Service path in this tutorial. It doesn't change the LangGraph, Semantic Kernel, or Microsoft Agent Framework implementations shown in the other tab.

Create or select a Foundry IQ knowledge base, and then connect the knowledge base to the Foundry Agent Service agent. The connection is exposed to the agent as a managed MCP knowledge tool.

The App Service code continues to invoke the same agent by name through its existing Foundry client and agent_reference. The web app doesn't need a direct Azure AI Search integration or its own MCP client. If the UI displays sources, process the citation annotations returned by the agent.

Which managed identity does each connection use?

Direction Identity
App Service calls Foundry App Service system-assigned identity
Foundry OpenAPI tool calls /api/tasks Parent Foundry resource system-assigned identity

The project endpoint selects the project and agent. It doesn't determine the identity that the hosted OpenAPI tool uses.

Clean up resources

When you're done with the application, you can delete the App Service resources to avoid incurring further costs:

azd down --purge

Then, delete the Foundry resource if you created it separately.

More resources