Webhooks let you trigger actions in other systems when something interesting happens in W&B, like a model finishing training or a metric crossing a threshold.

Here’s a W&B run that just finished training a simple PyTorch model on MNIST. Notice the hook argument in the wandb.init() call:

import wandb
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms

# --- W&B Initialization ---
run = wandb.init(
    project="webhook-demo",
    job_type="training",
    config={
        "learning_rate": 0.001,
        "epochs": 3,
        "batch_size": 64
    },
    # This is where the webhook magic happens!
    # We're telling W&B to send a POST request to this URL
    # when the run finishes.
    hooks=[
        wandb.hook(
            run_state="finished",
            url="https://example.com/my-webhook-receiver",
            method="POST",

            body='{"event": "wandb_run_finished", "run_id": "{{run.id}}", "project": "{{run.project.name}}", "run_url": "{{run.url}}"}'

        )
    ]
)

# --- Model Definition (Standard PyTorch) ---
class SimpleNN(nn.Module):
    def __init__(self):
        super(SimpleNN, self).__init__()
        self.fc1 = nn.Linear(28 * 28, 128)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(128, 10)

    def forward(self, x):
        x = x.view(-1, 28 * 28)
        x = self.fc1(x)
        x = self.relu(x)
        x = self.fc2(x)
        return x

# --- Data Loading ---
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])
train_dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=run.config.batch_size, shuffle=True)

# --- Training Setup ---
model = SimpleNN()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=run.config.learning_rate)

# --- Training Loop ---
for epoch in range(run.config.epochs):
    for i, (images, labels) in enumerate(train_loader):
        outputs = model(images)
        loss = criterion(outputs, labels)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        # Log metrics to W&B
        wandb.log({"loss": loss.item(), "epoch": epoch})

        if (i + 1) % 100 == 0:
            print(f'Epoch [{epoch+1}/{run.config.epochs}], Step [{i+1}/{len(train_loader)}], Loss: {loss.item():.4f}')

# --- Finish Run ---
# When this line is executed, W&B detects the 'finished' state
# and triggers the webhook defined in wandb.init().
wandb.finish()

When this script runs and wandb.finish() is called, W&B will send a POST request to https://example.com/my-webhook-receiver. The body of this request will be:

{
  "event": "wandb_run_finished",
  "run_id": "YOUR_RUN_ID",
  "project": "webhook-demo",
  "run_url": "https://wandb.ai/YOUR_USERNAME/webhook-demo/runs/YOUR_RUN_ID"
}

The {{run.id}}, {{run.project.name}}, and {{run.url}} are templated fields that W&B automatically populates with the actual values from the run. This allows you to dynamically include crucial information in your webhook payloads.

Beyond just "finished," you can trigger webhooks on several other run states:

  • created: When a run is first initialized.
  • running: While the run is actively executing.
  • resumed: If a run is resumed from a checkpoint.
  • failed: If the run encounters an unhandled exception.

You can also set up multiple webhooks for a single run, each with different triggers and payloads. For example, you might want one webhook to notify a Slack channel when a run finishes successfully and another to trigger an auto-scaling job if a run fails.

The body argument in wandb.hook supports Jinja2 templating, giving you fine-grained control over the data sent. You can access almost any attribute of the run object, including run.config, run.summary, run.user, and more. For instance, to include the final accuracy from run.summary:

# ... inside wandb.init()
hooks=[
    wandb.hook(
        run_state="finished",
        url="https://example.com/notify-accuracy",
        method="POST",

        body='{"run_id": "{{run.id}}", "final_accuracy": {{run.summary.accuracy}}}'

    )
]
# ...

This allows you to send specific, actionable data to your downstream systems.

The most surprising thing about W&B webhooks is how easily they integrate into existing CI/CD or MLOps workflows without requiring custom W&B agents or complex polling mechanisms. You define the trigger and the payload, and W&B handles the communication automatically as events occur. This declarative approach means less boilerplate code for you and more reliable event-driven automation.

If you need to send data to a system that only accepts GET requests, you can use the method="GET" and pass parameters via the url itself, like url="https://example.com/log_event?event=run_finished&run_id={{run.id}}". However, POST is generally preferred for sending structured data payloads.

The next step after setting up basic webhooks is to explore more advanced templating with Jinja2 and consider using W&B’s wandb.Api within your webhook receiver to pull additional run details.

Want structured learning?

Take the full Wandb course →