Skip to content
Novu logoNovu

Human-in-the-loop approval for a LangChain agent in Slack

Connect your LangChain email agent to Slack with Novu Connect and add human-in-the-loop approval to review email drafts before sending.

LangChain agent actions routed through Novu Connect for approval in Slack
Author:Victor Yakubu
Victor Yakubu

While reading LangChain’s guide to building an agent, I wondered how I could bring its email-agent example into Slack and review outgoing emails there before they were sent.

The guide includes human review before sending an email. You’ll build a small version of that workflow with Novu Connect. You’ll ask a TypeScript LangChain agent to send an email, review the proposed recipient and message in Slack, and approve or deny the send.

The email tool is mocked, so you can test the approval flow without connecting an email provider.

Key takeaways

Keep your LangChain logic in your application.

The model, instructions, tools, credentials, and business logic stay in your application. Novu Connect handles the conversation and approval flow between the agent and Slack.

Gate the action, not the conversation.

Use needsApproval to pause sendEmail only when the agent is ready to send. Drafting, revisions, and ordinary replies can continue without approval.

Review the complete email in Slack.

Show the proposed recipient, subject, and body in a native approval card. The reviewer can approve or deny the action without leaving the Slack thread.

Resume the agent automatically after a decision.

Novu records the decision in ctx.history and invokes onMessage again. LangChain continues the tool loop, running sendEmail only after approval.

Extend the same approval flow beyond Slack.

The same pattern works across Microsoft Teams, WhatsApp, Telegram, email, and Agent Chat, using the native controls available in each channel.

What it takes to add human approval to a LangChain agent

  1. 1

    Keep the agent logic in your application

    Your application continues to own the model, instructions, tools, credentials, and business logic. LangChain drafts the email and proposes the sendEmail action without requiring a separate Slack-specific agent.

  2. 2

    Let Novu Connect handle the approval loop

    Novu Connect pauses the proposed sendEmail call and shows the recipient, subject, and body in Slack. After the reviewer approves or denies the action, Novu records the decision and resumes the agent. The tool runs only after approval.

What you will build

You’ll start with an existing Next.js application and add a small LangChain email agent. The finished interaction works like this:

  1. You ask the agent in Slack to send an email.
  2. LangChain drafts the email and calls sendEmail with the recipient, subject, and body.
  3. Novu Connect posts an approval card in Slack and waits for your decision before allowing sendEmail to run.
  4. You approve or deny the proposed tool call.
  5. Novu records the decision in the conversation history and invokes the handler again.
  6. LangChain continues from that history. The mock email tool runs only when the call is approved.

With Novu Connect, a LangChain agent can post an approval card in Slack when a gated tool is called, then continue from the conversation history after the reviewer decides.

Approval flow from Slack through Novu Connect and LangChain to sendEmail

How the Slack approval flow works

Your application still owns the model, system prompt, tools, credentials, and business logic. Novu Connect provides the communication layer between that application and Slack.

The @novu/framework/langchain adapter connects both sides. Your onMessage handler returns a LangChainAgentConfig containing the model, prompt, and tools. When LangChain proposes a gated tool call, the adapter pauses the turn and Novu posts the approval card. After the reviewer decides, Novu invokes onMessage again, and the adapter maps the approval cycle from ctx.history back into LangChain messages.

This is the adapter-managed approval path. You do not need to call ctx.toolApproval.request() or add a separate LangGraph checkpointer for the implementation shown here. LangChain's native HumanInTheLoopMiddleware is a different implementation path that uses LangGraph persistence.

Prerequisites

This tutorial assumes that you have:

  • A Novu account.
  • Node.js 22 or later.
  • An existing Next.js application.
  • A Slack workspace where you can install an app.
  • An OpenAI API key.
  • Basic familiarity with TypeScript and LangChain tools.

The example uses the Next.js App Router and OpenAI because they match the current Novu LangChain quickstart. Novu also provides bridge adapters for other server frameworks.

Build the LangChain email agent

If your application already has a LangChain agent, keep its existing model, instructions, and tool logic. The main change is to return that configuration from a Novu agent() handler and register the handler on a bridge route.

For this tutorial, we will add one mocked sendEmail tool so the approval boundary remains easy to see.

  1. 1

    Install the packages

    Install the Novu framework, LangChain, the OpenAI provider package, Zod, and zod-to-json-schema (the framework's tool-schema conversion needs it, but it’s not pulled in automatically):

    npm install @novu/framework langchain @langchain/core @langchain/openai zod zod-to-json-schema

    The LangChain adapter is available from the @novu/framework/langchain import path.

    Add your model key to .env.local if the application does not already have it:

    .env.local
    OPENAI_API_KEY=your-openai-api-key

    The Novu CLI will add the Novu credentials when you connect the project later.

  2. 2

    Create the email tool

    Create app/novu/agents/email-agent.ts. Start with a LangChain tool that accepts the complete proposed email:

    app/novu/agents/email-agent.ts
    import { tool } from '@langchain/core/tools';
    import { agent } from '@novu/framework/langchain';
    import { z } from 'zod';
    
    const sendEmail = tool(
      async ({ to, subject, body }) => {
        console.log('Mock email sent', { to, subject, body });
    
        return {
          status: 'sent',
          to,
          subject,
        };
      },
      {
        name: 'sendEmail',
        description: 'Send an email after a person approves the recipient and content',
        schema: z.object({
          to: z.string().email(),
          subject: z.string(),
          body: z.string(),
        }),
      },
    );

    The function only logs the proposed email and returns a mock result. Replace its body with your email-service call when you adapt the example to your application.

  3. 3

    Require approval before the email is sent

    Add the Novu agent handler below the tool in the same file:

    app/novu/agents/email-agent.ts
    export const emailAgent = agent('email-agent', {
      onMessage: async () => ({
        model: 'openai:gpt-4o',
        system: [
          'You are an email assistant.',
          'Draft concise, professional emails from the user instructions.',
          'Use sendEmail only when the user asks you to send the message.',
        ].join(' '),
        tools: [sendEmail],
        needsApproval: (toolCall) => toolCall.name === 'sendEmail',
      }),
    });

    needsApproval evaluates each proposed tool call. It returns true for sendEmail, so Novu pauses that call before the tool function runs. Ordinary replies from the agent do not require approval.

    Keep the identifier email-agent consistent. It must match the agent identifier you connect to in Novu.

  4. 4

    Show the proposed email in the approval card

    A useful approval request should show the exact action under review. The tool-approval API lets you customize the native card while Novu continues to wire its approval actions.

    Add toolApproval before onMessage in the agent configuration:

    app/novu/agents/email-agent.ts
    export const emailAgent = agent('email-agent', {
      toolApproval: {
        renderApproval: ({ toolCall, approvalCard }) => {
          const input = toolCall.input as {
            to?: string;
            subject?: string;
            body?: string;
          };
    
          return approvalCard({
            title: 'Approve this email?',
            subtitle: `To: ${input.to ?? 'Unknown recipient'}`,
            body: [
              `*Subject:* ${input.subject ?? 'No subject'}`,
              '',
              input.body ?? 'No email body',
            ].join('\n'),
          });
        },
      },
    
      onMessage: async () => ({
        model: 'openai:gpt-4o',
        system: [
          'You are an email assistant.',
          'Draft concise, professional emails from the user instructions.',
          'Use sendEmail only when the user asks you to send the message.',
        ].join(' '),
        tools: [sendEmail],
        needsApproval: (toolCall) => toolCall.name === 'sendEmail',
      }),
    });

    The body field supports Slack markdown. This example displays the subject and draft beneath the recipient so the reviewer can inspect the proposed tool input before deciding.

  5. 5

    Register the bridge endpoint

    Export the agent from app/novu/agents/index.ts:

    app/novu/agents/index.ts
    export { emailAgent } from './email-agent';

    Then create app/api/novu/route.ts:

    app/api/novu/route.ts
    import { serve } from '@novu/framework/next';
    import { emailAgent } from '../../novu/agents';
    
    export const { GET, POST, OPTIONS } = serve({
      agents: [emailAgent],
    });

    The bridge is an HTTP endpoint in your application. Novu calls it when a Slack message arrives, your handler runs inside your application, and Novu delivers the result back to the Slack conversation.

Connect the LangChain agent to Slack

Run the Novu Connect CLI from the root of the existing project:

npx novu connect --runtime langchain --channel slack

Follow the guided flow to connect your Novu account and Slack workspace. Select or create the agent with the email-agent identifier used in the handler. In an existing project, the CLI installs the required Novu packages when needed, writes the Novu environment variables, and wires the project where it can. Depending on the application, it may leave the handler or bridge route for you to finish.

When a Next.js application uses LangChain model strings, the LangChain packages must be included in serverExternalPackages. The runtime-specific CLI configures this when it scaffolds the integration. The LangChain reference shows the manual configuration if you need to check it.

Novu Connect CLI showing a LangChain agent connected to Slack

Start the local bridge with the script created by the CLI:

npm run dev:novu

Keep this process running while you test the agent in Slack.

Test the approval flow in Slack

You can message the bot directly or mention it in a Slack channel. Novu keeps the reply in the same thread. Use a fictional recipient for the first test:

Email [email protected] to confirm that the project review is Thursday at 2 p.m.

The agent should draft the message and propose a sendEmail call. Because the tool matches needsApproval, its function should not run yet. Instead, Slack should show the approval card with the recipient, generated subject, and email body.

Slack approval card awaiting approval for the LangChain email agent

Approve the tool call

Approve the proposed action in Slack. Novu records the decision, invokes onMessage again, and the LangChain adapter resumes the tool loop from the updated conversation history.

You should now see Mock email sent in the application logs. The agent can use the tool result to reply in the Slack thread.

Langchain agent

Deny the tool call

Run a second test with a different fictional request:

Email [email protected] to move the project review to Friday morning.

When the approval card appears, deny the call. The sendEmail function should not produce a new log entry. The decision is added to the conversation history when the handler resumes, but the exact wording of the agent's next reply depends on the model and instructions.

Langchain agent in Slack

What happens after the reviewer decides

With the LangChainAgentConfig used in this tutorial and no custom onToolApproval handler, Novu handles the decision automatically. After you select Approve or Deny:

  1. Novu removes the approval card and restarts the typing indicator.
  2. It records your decision in ctx.history.
  3. It calls onMessage again, and the LangChain adapter uses the updated history to continue processing the request.

An approved sendEmail call can then run. A denied call does not execute.

Two additional rules apply while approval is pending:

  • Several calls need approval. Novu presents them individually. The next card appears after you resolve the current one.
  • You send another message before deciding. Novu automatically denies the pending calls, removes the card, and processes your new message.

You can add an onToolApproval handler when you need custom behavior after a click. If you do, your application becomes responsible for editing or deleting the approval card.

Apply approval to the action that needs it

The example gates sendEmail, not every message the agent produces. The agent can still discuss a draft, revise the subject, or answer a question without interrupting the conversation. Approval appears only when LangChain proposes the tool that performs the send.

This distinction keeps the interaction focused. A reviewer sees an approval card at the point where the agent is ready to take the external action, with the proposed recipient and content available in the same Slack thread.

If you add more tools, then expand the predicate only for the calls that should pause:

needsApproval: (toolCall) =>
  ['sendEmail', 'forwardEmail'].includes(toolCall.name)

The predicate belongs to your application configuration, so the application continues to define which LangChain tools require a decision.

Extend the approval flow beyond Slack

The same Novu tool-approval capability is available on Microsoft Teams, WhatsApp, Telegram, email, and Agent Chat. Each channel renders the interaction using the controls it supports. For example, Slack uses a native block, Microsoft Teams uses an Adaptive Card, and WhatsApp uses quick replies.

Channel-native approvals

Novu brings the same approval flow to Microsoft Teams, WhatsApp, Telegram, email, and Agent Chat. Each channel renders it with native controls—from Slack blocks and Teams Adaptive Cards to WhatsApp quick replies.

Give your LangChain agent a Slack approval step

You now have a LangChain email agent that accepts instructions in Slack, drafts an email, and pauses before sendEmail runs. The agent logic and tool execution remain in your application, while Novu carries the conversation and approval decision between your handler and Slack.

Run the Connect command inside your LangChain project to set up the same path:

npx novu connect --runtime langchain --channel slack

Continue with the LangChain quickstart or review the complete tool-approval API.

Approve LangChain agent actions in Slack

Use Novu Connect to pause sendEmail until a reviewer approves it.

Start with LangChain

Frequently asked questions

These answers cover the implementation choices developers are likely to encounter when adding LangChain human-in-the-loop approval to Slack.

What is human-in-the-loop approval in LangChain?

Human-in-the-loop approval pauses a proposed agent action before it runs and asks a person to approve or deny it. In this tutorial, the approval applies only to the LangChain sendEmail tool.

How do I require approval before a LangChain tool runs?

Return true from the needsApproval predicate for the tool call that should pause. In this example, the predicate matches sendEmail, so Novu requests approval before the tool function runs.

Does this Slack approval flow require a LangGraph checkpointer?

No. The @novu/framework/langchain adapter manages the approval cycle shown here. LangChain's native HumanInTheLoopMiddleware is a different implementation path that uses LangGraph persistence.

What happens when a reviewer denies the tool call?

Novu records the denial in ctx.history and invokes onMessage again. The denied tool does not run, and the agent can continue the conversation using the updated history.

Can I require approval for only some LangChain tools?

Yes. Keep needsApproval as a predicate and return true only for the tool names or inputs that should pause. Ordinary agent replies and ungated tools continue without approval.

Can I show the email draft in the Slack approval card?

Yes. Use toolApproval.renderApproval with approvalCard to show the recipient, subject, and body. The body supports Slack markdown, so the reviewer can inspect the complete proposal before deciding.

Can the same approval flow work outside Slack?

Yes. Novu supports tool approval across Microsoft Teams, WhatsApp, Telegram, email, and Agent Chat, using the native controls available in each channel.

Read More

You’re five minutes away from your first Novu-backed notification

Create a free account, send your first notification, all before your coffee gets cold... no credit card required.