DEV Community

Cover image for Completing the Loop: A Developer’s Guide to Slack Incoming Webhooks
Matt Mochalkin
Matt Mochalkin

Posted on

Completing the Loop: A Developer’s Guide to Slack Incoming Webhooks

In our previous article, we delved into the mechanics of receiving real-time notifications from Slack using a webhook and handling the incoming data with a Symfony controller or symfony/webhook component. This was the foundational step — the “ear” of our system.

Now, we’re ready to move from passive listening to proactive action.

This article will guide you through the process of building a Slack auto-answer system. This is the next evolution of our “proactive agent”, which will not only receive event data but also analyze it and send an automated response back to the channel. This is a crucial step towards creating a truly intelligent and responsive bot that can handle routine queries, provide instant updates, or trigger further actions based on specific keywords or events.

We’ll focus on the architecture and key components needed to build this system, leveraging Symfony’s power to process the incoming webhook data and interact with the Slack API to send messages. Get ready to turn your simple webhook receiver into a powerful real-time automation tool.

To kick things off, we need to set up the communication channel for our automated responses. We’ll enable our Slack workspace to receive messages from our application. This is done by configuring Incoming Webhooks within the Slack API developer console at https://api.slack.com.

An Incoming Webhook is a simple way for an external application to post messages into Slack. It provides a unique URL that acts as a secure, one-way bridge. When our Symfony application sends a message (payload) to this URL, Slack will receive it and post it in the designated channel.

Slack Incoming Webhook Creation

To create an Incoming Webhook in Slack, you need to follow these steps:

1. Select your Slack App or create a new one
Go to api.slack.com/apps and click “Create an App”.

Choose a name for your app and select the Slack workspace where you want to install it. It’s a good idea to use a dedicated development or testing workspace to avoid spamming your main channels.

2. Activate Incoming Webhooks
In the left-hand sidebar, navigate to “Features” > “Incoming Webhooks”.

Toggle the “Activate Incoming Webhooks” switch to “On”.

3. Add a New Webhook to Your Workspace
Once activated, the page will refresh and you’ll see a new section.

Scroll down and click “Add New Webhook to Workspace”.

A new page will appear, prompting you to choose the channel where your app will post messages. Select a channel and click “Authorize”.

4. Get the Webhook URL
After authorization, you’ll be returned to the app’s settings page.

A new Webhook URL will be listed under the “Webhook URLs for Your Workspace” section.

Copy this URL. This is the unique endpoint that your Symfony application will use to send messages to Slack. Remember to treat this URL like a password and keep it secure (e.g., using environment variables).

How to Send Your First Message

Once you have the webhook URL, you can use a command-line tool like curl to test the connection and send your first message from the console. This is a great way to confirm that your Slack Incoming Webhook is set up correctly before you start integrating it into your application.

Here is a basic curl command to send a JSON message to your Slack channel:

curl -X POST -H 'Content-type: application/json' - data '{"text":"Hello from our Proactive Agent! 🤖"}' [YOUR_SLACK_WEBHOOK_URL]
Enter fullscreen mode Exit fullscreen mode

Let’s break down this command:

curl: The command-line tool for transferring data with URLs.

-X POST: Specifies the HTTP request method as POST. This tells the server you’re sending data to it.

-H ‘Content-type: application/json’: Sets the Content-Type header. This tells Slack the data you’re sending in the request body is in JSON format. If you omit this, the request will likely fail.

— data ‘{“text”:”Hello from our Proactive Agent! 🤖”}’: This flag, and its alias -d, specifies the data to be sent in the request body. The message payload is a simple JSON object with a single key, “text”, and its value, which is the message you want to appear in Slack.

[YOUR_SLACK_WEBHOOK_URL]: This is the unique URL you copied from your Slack app’s developer settings.

After running this command in your terminal, you should see a new message appear in the Slack channel you configured for the webhook. This confirms that the one-way communication from your external system to Slack is working perfectly. The next step is to integrate this functionality directly into our Symfony application

Discover Solutions

Let’s analyze the options for sending messages from a Symfony application. We have four main choices: using the external curl utility, Symfony’s HttpClient, the Guzzle library, or the Symfony Notifier component.

1. Using curl from the Command Line

Pros:

Simple & Fast: It’s the quickest way to test and verify a connection.

No Dependencies: You don’t need to install any PHP packages, as curl is a standard system utility.

Platform Agnostic: It works the same on Linux, macOS, and Windows.

Cons:

Security Risk: Executing an external command via PHP’s exec() or shell_exec() functions can introduce security vulnerabilities if not handled carefully, especially with user-provided data.

Performance Overhead: Spawning a new process for each request is less efficient than using a native PHP library.

Poor Error Handling: It’s more difficult to parse and handle errors from the command line output within your PHP code. This method is generally not recommended for production environments.

2. Symfony/http-client

Pros:

Native to Symfony: It’s the official, built-in solution for making HTTP requests. It integrates seamlessly with the framework’s architecture, including its event dispatcher and configuration.

Performance: It’s a high-performance, asynchronous HTTP client.

Robust & Secure: It provides a safe and reliable way to handle requests, with features for retries, redirects, and comprehensive error handling.

Minimal Configuration: It’s easy to set up and use with minimal boilerplate code.

Cons:

Symfony-Specific: While you can use it outside of a full Symfony project, it’s designed to work best within the framework ecosystem.

3. Guzzle

Pros:

Most Popular: Guzzle is the de facto standard HTTP client in the PHP community. Many packages and APIs use it under the hood.

Rich Feature Set: It offers a wide range of advanced features, including middleware, a robust plugin system, and extensive configuration options.

Framework Agnostic: It’s a standalone library that can be used with any PHP project, not just Symfony.

Cons:

External Dependency: You have to install an extra package (guzzlehttp/guzzle), which adds a layer of complexity.

Can Be Overkill: For simple tasks like sending a webhook, its extensive feature set might be more than you need.

4. Symfony Notifier Component

Pros:

High-Level Abstraction: This is the most developer-friendly option. It provides a clean, abstract way to send notifications without worrying about the underlying transport. It treats Slack as just another channel (like email, SMS, or Telegram).

Built-in Integrations: It comes with pre-built “bridge” packages for popular services like Slack. You just configure your DSN and a Message object, and the component handles the rest.

Easy to Switch: If you decide to send the same message to a different service (e.g., from Slack to an email), you only need to change the DSN, not the entire code.

Cons:

Requires More Setup: It’s a higher-level solution that requires installing two packages: symfony/notifier and the symfony/slack-notifier.

Limited Customization: While it’s great for simple messages, it can be less flexible if you need to access very specific, low-level Slack API features that the notifier component doesn’t support out of the box.

Top Pick

For a Symfony project, the symfony/http-client is the best choice for this task. It’s the official, high-performance, and secure solution that fits perfectly within the framework.

However, if your goal is to have a highly reusable and abstract notification system, the Symfony Notifier Component is the superior option, as it simplifies the process of sending messages to multiple services. We’ll proceed with this component in the next steps as it represents a more “Symfony-way” of handling notifications.

Symfony Slack Notifier Component

Since we’ve already installed the symfony/notifier component, to solve our task we just need to install the Slack bridge. This will allow the Notifier to “know” how to send notifications via Slack.

To install the necessary package, run the following command in your terminal:

composer require symfony/slack-notifier
Enter fullscreen mode Exit fullscreen mode

This command will install the symfony/slack-notifier package, which provides the integration with the Slack API. This will allow you to use the Notifier to send messages to your channel without having to worry about the low-level details of HTTP requests and JSON message formatting.

With this package, you can now use this component within your Symfony application to send messages in an abstract and clean way.

For this example, we’ll update the config/packages/notifier.yaml file simple and focused on the email and slack channels to avoid over-complicating things.

framework:
    notifier:
        message_bus: core.command.bus
        chatter_transports:
            slack: '%env(SLACK_DSN)%'
        texter_transports:
        channel_policy:
            urgent: [!php/const App\Const\NotificationChannel::EMAIL]
            high: [!php/const App\Const\NotificationChannel::SLACK]
            medium: [!php/const App\Const\NotificationChannel::EMAIL]
            low: [!php/const App\Const\NotificationChannel::EMAIL]
Enter fullscreen mode Exit fullscreen mode

Next, you need to add the SLACK_DSN variable to your .env file. This is [YOUR_SLACK_WEBHOOK_URL].

SLACK_DSN=YOUR_SLACK_WEBHOOK_URL
Enter fullscreen mode Exit fullscreen mode

Based on the our previous configuration, the Symfony Notifier component is set to send all notifications to an email address, with the exception of high-importance notifications. This allows us to strategically route urgent messages.

Our next step is to configure our Slack responses to be of high importance. By doing this, we can ensure that any automated replies we generate are sent directly to the Slack channel, bypassing the default email transport. This provides a clean and logical separation of communication channels: standard notifications go to email, while real-time, high-priority responses are delivered to Slack.

This approach is highly effective for building a proactive agent because it enables you to send routine, low-priority alerts to one channel and reserve the Slack channel for critical, time-sensitive information, such as immediate answers to user questions or alerts about system failures.

By following this strategy, we maintain a clear and efficient communication flow, ensuring that the right message reaches the right channel at the right time.

Webhook Processing — RemoteEvent

This is a main step for our “Proactive Agent” because it provides the mechanism for our system to take action — to send automated replies, confirmations, or other relevant information back to the user or channel. This completes the communication loop: receive an event via webhook and send a response via webhook.

Now, let’s create a RemoteEvent named slack_webhook_processing to handle the incoming request from Slack correctly.

Symfony/webhook component and the Symfony/remote-event component work together to provide a robust and secure way to handle webhooks. The Webhook component’s job is to receive the HTTP request from the external service (in this case, Slack), validate it (using a secret, for example), and then create a RemoteEvent object from the request payload.

The RemoteEvent object is a simple PHP class that serves as an abstraction for the external event. It has three main properties:

$id: A unique identifier for the event.

$name: The name of the event (e.g., slack_webhook_processing).

$payload: An array containing all the data from the incoming request.

By creating a RemoteEvent named slack_webhook_processing, we are giving our application a clear, internal representation of the Slack webhook.

This allows us to decouple the logic of processing the event from the logic of receiving and validating the webhook itself. The RemoteEvent is then passed to a consumer (a service that listens for and acts on this specific event), where we can implement our custom logic for responding to the Slack message.

This architecture ensures that our application’s core logic remains clean and focused solely on the event data, without needing to worry about HTTP requests, headers, or security validation.

It’s a “Symfony-way” of handling external events, making your code more maintainable and testable.

Using Symfony Messenger for Asynchronous Slack Notification Handlin

g
To ensure our system is performant and responsive, we’ll process the incoming Slack notifications asynchronously using Symfony Messenger. This means our webhook controller can immediately return a 200 OK response to Slack, while the actual processing — like communicating with the LLM service — happens in the background.

This process involves two key components: a message that represents the data we want to process and a handler that contains the business logic to act on that message.

1. Creating the Message
First, let’s create a new message class AIAgentActionMessage similar to AIAgentSummarizeMessage. This message will encapsulate the necessary data from the Slack webhook payload (SlackEvent), which our handler will need to perform its task. A good practice is to make the message class an immutable data object.

namespace App\Message\Command;

use App\DTO\DataCollection;
use Symfony\Component\Notifier\Notification\Notification;

readonly class AIAgentActionMessage {
    public function __construct(
        private DataCollection $dataCollection,
        private string $prompt,
        private string $from,
        private string $replyTo,
        private string $subject,
        private string $notificationImportance = Notification::IMPORTANCE_MEDIUM
    )
    {
    }

    public function getDataCollection(): DataCollection
    {
        return $this->dataCollection;
    }

    public function getPrompt(): string
    {
        return $this->prompt;
    }

    public function getFrom(): string
    {
        return $this->from;
    }

    public function getReplyTo(): string
    {
        return $this->replyTo;
    }

    public function getSubject(): string
    {
        return $this->subject;
    }
    public function getNotificationImportance(): string
    {
        return $this->notificationImportance;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, in our RemoteEvent class we’ll dispatch this message to the message bus:

namespace App\RemoteEvent;

use App\DTO\DataCollection;
use App\DTO\Slack\SlackEvent;
use App\DTO\SlackMessage;
use App\Message\Command\AIAgentActionMessage;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\RemoteEvent\Attribute\AsRemoteEventConsumer;
use Symfony\Component\RemoteEvent\Consumer\ConsumerInterface;
use Symfony\Component\RemoteEvent\RemoteEvent;

#[AsRemoteEventConsumer('slack_webhook_processing')]
final readonly class SlackWebhookConsumer implements ConsumerInterface
{
    public function __construct(private MessageBusInterface $messageBus)
    {
    }

    public function consume(RemoteEvent $event): void
    {
        if (array_key_exists('entity', $event->getPayload()) && ($event->getPayload()['entity'] instanceof SlackEvent)) {
            $entity = $event->getPayload()['entity'];

            $slackCollection = new DataCollection(
                new SlackMessage(
                    'New Slack message',
                    $entity->getEvent()->getUser(),
                    $entity->getEvent()->getTeam(),
                    $entity->getEvent()->getText(),
                    $entity->getEvent()->getTs(),
                    $entity->getEvent()->getClientMsgId()
                )
            );

            $this->messageBus->dispatch(
                new Envelope(
                    new AIAgentActionMessage(
                        $slackCollection,
                        'I have slack message. Please generate reply it into a concise overview (100-150 words) focusing on key decisions, action items, and deadlines. Here’s the slack message content:',
                        $entity->getEvent()->getTeam(),
                        $entity->getEvent()->getUser(),
                        ''
                    )
                )
            );
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Creating the Message Handler
The handler is a service that “listens” for messages of a specific type (AIAgentActionMessage in our case) and executes the logic. This is where we’ll integrate the existing LLM service and the Notifier component.

The handler AIAgentActionMessageHandler will receive the AIAgentActionMessage message, use the injected LLM service (GeminiAIAgentService or OpenAIAgentService)to get a response, and then use the Notifier to send the answer back to Slack.

namespace App\Handler\Command;

use App\DTO\MailMessage;
use App\Message\Command\AIAgentActionMessage;
use App\Message\Command\NotifySummarizedMessage;
use App\Service\AIAgentService;
use App\Service\AIProvider\GeminiAIAgentService;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;

#[AsMessageHandler(bus: 'core.command.bus', fromTransport: 'main.transport')]
readonly class AIAgentActionMessageHandler
{
    public function __construct(
        private AIAgentService $aiAgentService,
        private GeminiAIAgentService $geminiAIAgentService,
        protected MessageBusInterface $messageBus
    ){
    }

    public function __invoke(AIAgentActionMessage $message): void
    {
        $result = $this->aiAgentService->action($this->geminiAIAgentService, $message->getDataCollection(), $message->getPrompt());

        if (!is_null($result)) {
            $this->messageBus->dispatch(
                new Envelope(
                    new NotifySummarizedMessage(
                        new MailMessage(
                            $message->getSubject(),
                            $message->getFrom(),
                            $message->getReplyTo(),
                            $result,
                            null
                        ),
                        $message->getNotificationImportance()
                    )
                )
            );
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

By using this asynchronous approach with Symfony Messenger, we ensure that our application remains highly responsive, even when dealing with potentially slow external services like a large language model. It’s a key architectural decision for building a robust and scalable “Proactive Agent.”

Now that our code is complete, we’re ready to deploy and run our application. For our Proactive Agent system, we’ll use a containerized approach with Docker Compose, which simplifies the process of managing both our Symfony application and its dependencies, such as a database or message broker.

Conclusion: From Passive Listener to Proactive Agent

In this two-part series, we’ve transformed a simple webhook receiver into a powerful real-time auto-answer system. We began by establishing the foundation: listening for events from Slack using a Symfony controller and symfony/webhook. In this article, we completed the communication loop by building the outbound messaging system.

We leveraged Slack Incoming Webhooks to create a secure, one-way channel for our application to send messages back to users. By integrating Symfony Messenger, we ensured our system is asynchronous and highly performant, offloading heavy processing tasks like communicating with an LLM to the background.

Finally, we used the Symfony Notifier component to create a clean, abstract, and scalable way to deliver our automated responses.

The result is a robust, event-driven application that not only detects user activity but proactively responds to it. This “Proactive Agent” is a prime example of how modern, decoupled architecture can be used to build intelligent and responsive applications.

The principles we’ve applied — asynchronous processing, message-based communication, and service abstraction — are foundational to modern application development. You can take this foundation and build upon it: integrate a more complex routing system, connect to a database to provide personalized answers, or even add more services to your “Proactive Agent” to perform complex tasks with different LLM Models. The possibilities are endless.

I am ready to provide code examples for:

  • curl: Demonstrating a direct command-line approach.
  • symfony/http-client: Using Symfony’s built-in, high-performance HTTP client.
  • Guzzle: The most popular standalone PHP HTTP client.
  • Dynamic Symfony Notifier Channels: Showing how to programmatically create multiple notification channels without notifier.yaml.

Just let me know which examples you’d like to see, and I will prepare them for you.

Stay tuned — and let’s keep the conversation going.

Top comments (0)