Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/configuration/tools/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Built-in tools are included with docker-agent and require no external dependenci
| `open_url` | Open a fixed URL in the user's default browser | [Open URL](../../tools/open-url/index.md) |
| `transfer_task` | Delegate to sub-agents (auto-enabled) | [Transfer Task](../../tools/transfer-task/index.md) |
| `background_agents` | Parallel sub-agent dispatch | [Background Agents](../../tools/background-agents/index.md) |
| `webhook` | Send outbound notifications (Slack, Discord, Telegram, IFTTT, Teams, …) | [Webhook](../../tools/webhook/index.md) |
| `handoff` | Local conversation handoff to another agent in the same config (auto-enabled by `handoffs:`) | [Handoff](../../tools/handoff/index.md) |
| `a2a` | A2A remote agent connection | [A2A](../../tools/a2a/index.md) |
| `mcp_catalog` | Discover and activate remote MCP servers from the Docker MCP Catalog on demand | [MCP Catalog](../../tools/mcp-catalog/index.md) |
Expand Down
51 changes: 51 additions & 0 deletions docs/tools/webhook/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
title: "Webhook Tool"
description: "Send outbound notifications to Slack, Discord, Telegram, IFTTT, and more."
keywords: docker agent, ai agents, tools, toolsets, webhook, slack, discord, telegram, ifttt
linkTitle: "Webhook"
weight: 145
canonical: https://docs.docker.com/ai/docker-agent/tools/webhook/
---

_Send outbound notifications to Slack, Discord, Telegram, IFTTT, and more._

## Overview
The webhook toolset lets an agent POST a message to a webhook, shaping the JSON payload for the target service. Delivery is one-way — the tool reports the HTTP status, not a response body. It uses the SSRF-safe HTTP client (requests to non-public addresses are refused), and pairs naturally with the [`scheduler`](../scheduler/index.md) tool for alerting.

## Configuration
```yaml
toolsets:
- type: webhook
```
No configuration options.

## `send_webhook`
| Parameter | Required | Description |
| --- | --- | --- |
| `url` | Yes | The webhook URL to POST to. |
| `message` | Yes | The message text. |
| `provider` | No | Payload format (default `generic`). |
| `value2`, `value3` | No | IFTTT extra fields (`provider=ifttt`). |
| `chat_id` | No | Telegram chat id (`provider=telegram`). |

## Providers
| Provider | Payload |
| --- | --- |
| `slack`, `mattermost`, `rocketchat`, `googlechat`, `teams`, `generic` | `{"text": message}` |
| `discord` | `{"content": message}` |
| `ifttt` | `{"value1": message, "value2": …, "value3": …}` |
| `telegram` | `{"chat_id": …, "text": message}` |

## Example
```yaml
agents:
root:
model: openai/gpt-5-mini
instruction: If a scheduled check fails, notify the team via send_webhook.
toolsets:
- type: webhook
- type: scheduler
```

> [!TIP]
> Combine with the [`scheduler`](../scheduler/index.md): "every 15 minutes, check the build; if it broke, `send_webhook` to Slack."
1 change: 1 addition & 0 deletions pkg/teamloader/toolsets/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ var BuiltinToolsets = []BuiltinToolsetInfo{
builtinToolset("think", "think", "Step-by-step reasoning scratchpad for planning"),
builtinToolset("todo", "todo", "Manage a task list for complex multi-step workflows"),
builtinToolset("user_prompt", "user-prompt", "Ask the user questions and collect interactive input"),
builtinToolset("webhook", "webhook", "Send outbound notifications (Slack, Discord, Telegram, IFTTT, Teams, and more)"),
}

func builtinToolset(toolsetType, docsSlug, summary string) BuiltinToolsetInfo {
Expand Down
4 changes: 4 additions & 0 deletions pkg/teamloader/toolsets/toolsets.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/docker/docker-agent/pkg/tools/builtin/think"
"github.com/docker/docker-agent/pkg/tools/builtin/todo"
"github.com/docker/docker-agent/pkg/tools/builtin/userprompt"
"github.com/docker/docker-agent/pkg/tools/builtin/webhook"
"github.com/docker/docker-agent/pkg/tools/mcp"
)

Expand Down Expand Up @@ -113,5 +114,8 @@ func DefaultToolsetCreators() map[string]teamloader.ToolsetCreator {
"rag": func(ctx context.Context, toolset latest.Toolset, parentDir string, runConfig *config.RuntimeConfig, _ string) (tools.ToolSet, error) {
return rag.CreateToolSet(ctx, toolset, parentDir, runConfig)
},
"webhook": func(_ context.Context, _ latest.Toolset, _ string, runConfig *config.RuntimeConfig, _ string) (tools.ToolSet, error) {
return webhook.CreateToolSet(runConfig)
},
}
}
166 changes: 166 additions & 0 deletions pkg/tools/builtin/webhook/webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package webhook

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"

"github.com/docker/docker-agent/pkg/config"
"github.com/docker/docker-agent/pkg/httpclient"
"github.com/docker/docker-agent/pkg/tools"
)

const (
ToolNameSendWebhook = "send_webhook"

category = "webhook"
requestTimeout = 30 * time.Second
maxRespRead = 64 << 10
)

type httpDoer interface {
Do(req *http.Request) (*http.Response, error)
}

type ToolSet struct {
client httpDoer
}

var (
_ tools.ToolSet = (*ToolSet)(nil)
_ tools.Instructable = (*ToolSet)(nil)
)

func New() *ToolSet {
return &ToolSet{client: httpclient.NewSafeClient(requestTimeout, false)}
}

func CreateToolSet(_ *config.RuntimeConfig) (tools.ToolSet, error) {
return New(), nil
}

var supportedProviders = []string{
"slack", "discord", "ifttt", "telegram",
"mattermost", "rocketchat", "googlechat", "teams", "generic",
}

type SendArgs struct {
URL string `json:"url" jsonschema:"The webhook URL to POST to"`
Message string `json:"message" jsonschema:"The message text to send"`
Provider string `json:"provider,omitempty" jsonschema:"Payload format: slack, discord, ifttt, telegram, mattermost, rocketchat, googlechat, teams, or generic (default generic)"`
Value2 string `json:"value2,omitempty" jsonschema:"IFTTT value2 field (provider=ifttt only)"`
Value3 string `json:"value3,omitempty" jsonschema:"IFTTT value3 field (provider=ifttt only)"`
ChatID string `json:"chat_id,omitempty" jsonschema:"Telegram chat id (provider=telegram only)"`
}

func normalizeProvider(p string) string {
switch p = strings.ToLower(strings.TrimSpace(p)); p {
case "":
return "generic"
case "google_chat", "gchat":
return "googlechat"
case "msteams", "microsoftteams", "microsoft_teams":
return "teams"
case "rocket.chat", "rocket_chat":
return "rocketchat"
default:
return p
}
}

func buildPayload(provider, message, value2, value3, chatID string) (string, []byte, error) {
var payload map[string]string
switch normalizeProvider(provider) {
case "generic", "slack", "mattermost", "rocketchat", "googlechat", "teams":
payload = map[string]string{"text": message}
case "discord":
payload = map[string]string{"content": message}
case "ifttt":
payload = map[string]string{"value1": message}
if value2 != "" {
payload["value2"] = value2
}
if value3 != "" {
payload["value3"] = value3
}
case "telegram":
if strings.TrimSpace(chatID) == "" {
return "", nil, fmt.Errorf("telegram requires chat_id")
}
payload = map[string]string{"chat_id": chatID, "text": message}
default:
return "", nil, fmt.Errorf("unknown provider %q (supported: %s)", provider, strings.Join(supportedProviders, ", "))
}
body, err := json.Marshal(payload)
if err != nil {
return "", nil, err
}
return "application/json", body, nil
}

func (t *ToolSet) send(ctx context.Context, args SendArgs) (*tools.ToolCallResult, error) {
if strings.TrimSpace(args.URL) == "" {
return tools.ResultError("Error: url is required."), nil
}
if strings.TrimSpace(args.Message) == "" {
return tools.ResultError("Error: message is required."), nil
}
if u, err := url.Parse(args.URL); err != nil || (u.Scheme != "http" && u.Scheme != "https") {
return tools.ResultError("Error: url must be a valid http(s) URL."), nil
}

contentType, body, err := buildPayload(args.Provider, args.Message, args.Value2, args.Value3, args.ChatID)
if err != nil {
return tools.ResultError("Error: " + err.Error()), nil
}

req, err := http.NewRequestWithContext(ctx, http.MethodPost, args.URL, bytes.NewReader(body))
if err != nil {
return tools.ResultError("Error: " + err.Error()), nil
}
req.Header.Set("Content-Type", contentType)

resp, err := t.client.Do(req)
if err != nil {
return tools.ResultError("Error: sending webhook: " + err.Error()), nil
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, maxRespRead))

if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return tools.ResultError(fmt.Sprintf("Webhook returned HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(respBody)))), nil
}
return tools.ResultSuccess(fmt.Sprintf("Delivered to %s webhook (HTTP %d).", normalizeProvider(args.Provider), resp.StatusCode)), nil
}

func (t *ToolSet) Tools(context.Context) ([]tools.Tool, error) {
return []tools.Tool{
{
Name: ToolNameSendWebhook,
Category: category,
Description: "Send a message to a webhook (Slack, Discord, IFTTT, or a generic URL). POSTs a provider-shaped JSON payload and reports delivery status.",
Parameters: tools.MustSchemaFor[SendArgs](),
OutputSchema: tools.MustSchemaFor[string](),
Handler: tools.NewHandler(t.send),
Annotations: tools.ToolAnnotations{Title: "Send Webhook"},
AddDescriptionParameter: true,
},
}, nil
}

func (t *ToolSet) Instructions() string {
return `## Webhook Tool

Send an outbound notification with send_webhook(url, message, provider?):

- provider shapes the payload — slack, discord, ifttt, telegram, mattermost,
rocketchat, googlechat, teams, or generic (default). Telegram needs chat_id.
- Delivery is one-way; the tool reports the HTTP status, not a response body.
- Requests to non-public addresses are refused.`
}
Loading