[bug]: Issue deletion webhooks never fire (create/update work)
Is there an existing issue for this?
- [x] I have searched the existing issues
Current behavior
Issue deletion webhooks never fire (create/update work). External integrations such as Mattermost only receive the issue.created event; the delete action is missing. Confirmed on branch e8bdc47d6af6c39454a795a59098c0f2c34330a8 using the Docker local stack (docker-compose-local.yml). Database webhook_logs shows only action = 'created'; the delete event is absent.
| Action | Expected webhook? | Actual |
|---|---|---|
| Create | ✅ | ✅ |
| Update | ✅ | ✅ |
| Delete | ✅ | ❌ |
Verification query:
SELECT event_type, request_body->>'action' AS action
FROM webhook_logs
ORDER BY created_at DESC
LIMIT 5;
Output:
event_type | action
------------+--------
issue | created
(1 row)
Steps to reproduce
-
Checkout a fresh workspace
git checkout e8bdc47d6af6c39454a795a59098c0f2c34330a8 docker compose -f docker-compose-local.yml down -v docker compose -f docker-compose-local.yml up -d plane-db plane-redis plane-mq docker compose -f docker-compose-local.yml run --rm migrator docker compose -f docker-compose-local.yml up -d --build api worker beat-worker -
Seed a minimal workspace/project/webhook (inside
apicontainer)docker compose -f docker-compose-local.yml exec -T api python - <<'PY' import os, uuid from django.utils import timezone from django.contrib.auth.hashers import make_password os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.local") import django; django.setup() from plane.db.models.api import APIToken from plane.db.models.project import Project, ProjectIdentifier, ProjectMember from plane.db.models.state import State from plane.db.models.user import User from plane.db.models.workspace import Workspace, WorkspaceMember from plane.db.models.webhook import Webhook, ProjectWebhook from plane.db.models.issue_type import IssueType, ProjectIssueType from plane.license.models import Instance, InstanceAdmin email, password, slug, identifier = "[email protected]", "Passw0rd!", "test-workspace", "TST" instance, _ = Instance.objects.get_or_create( defaults={ "instance_name": "Local Instance", "instance_id": uuid.uuid4().hex, "current_version": "0.0.0", "last_checked_at": timezone.now(), } ) instance.is_setup_done = True instance.save() user, _ = User.objects.get_or_create( email=email, defaults={ "username": uuid.uuid4().hex, "first_name": "Admin", "last_name": "User", "password": make_password(password), "is_active": True, "is_superuser": True, }, ) InstanceAdmin.objects.get_or_create(user=user, instance=instance) workspace, _ = Workspace.objects.get_or_create( slug=slug, defaults={"name": "Test Workspace", "owner": user}, ) WorkspaceMember.objects.get_or_create(workspace=workspace, member=user, defaults={"role": 20}) project, _ = Project.objects.get_or_create( workspace=workspace, identifier=identifier, defaults={"name": "Test Project"}, ) ProjectIdentifier.objects.get_or_create(project=project, defaults={"workspace": workspace, "name": identifier}) ProjectMember.objects.get_or_create(project=project, member=user, defaults={"role": 20}) state, _ = State.objects.get_or_create( workspace=workspace, project=project, name="Todo", defaults={"color": "#FF0000", "group": "backlog", "default": True}, ) if project.default_state_id != state.id: project.default_state = state project.save(update_fields=["default_state"]) issue_type, _ = IssueType.objects.get_or_create( workspace=workspace, name="Task", defaults={"is_default": True}, ) ProjectIssueType.objects.get_or_create(project=project, issue_type=issue_type, defaults={"is_default": True}) webhook, _ = Webhook.objects.get_or_create( workspace=workspace, url="http://plane-webhook-receiver:8888/webhook", defaults={"is_active": True, "issue": True, "is_internal": True}, ) ProjectWebhook.objects.get_or_create(project=project, webhook=webhook) token, _ = APIToken.objects.get_or_create( user=user, workspace=workspace, defaults={"label": "local-test"}, ) print("API_TOKEN:", token.token) print("WORKSPACE_SLUG:", slug) print("PROJECT_ID:", project.id) PYNote the
API_TOKEN,WORKSPACE_SLUG, andPROJECT_IDprinted here. -
Run a simple webhook receiver (new terminal)
docker rm -f plane-webhook-receiver >/dev/null 2>&1 || true docker run --rm --name plane-webhook-receiver --network plane_dev_env python:3.12-slim \ python -u - <<'PY' import json, http.server, socketserver class Handler(http.server.BaseHTTPRequestHandler): def do_POST(self): payload = json.loads(self.rfile.read(int(self.headers['Content-Length']))) print("\n" + "="*50) print("Webhook received") print("Action:", payload.get("action")) print("Body:", json.dumps(payload, indent=2)) print("="*50 + "\n") self.send_response(200); self.end_headers() self.wfile.write(b'{"status": "ok"}') def do_GET(self): self.send_response(200); self.end_headers() self.wfile.write(b'{"status": "ok"}') def log_message(self, *_): pass socketserver.TCPServer(("", 8888), Handler).serve_forever() PY -
Create / update / delete an issue via API
TOKEN=<API_TOKEN from step 2> WORKSPACE=test-workspace PROJECT=<PROJECT_ID from step 2> docker compose -f docker-compose-local.yml exec -T api python - <<PY import json, urllib.request TOKEN="${TOKEN}" WORKSPACE="${WORKSPACE}" PROJECT="${PROJECT}" BASE="http://localhost:8000/api/v1" def call(method, path, payload=None): data = json.dumps(payload).encode() if payload else None req = urllib.request.Request( f"{BASE}{path}", data=data, headers={"Content-Type": "application/json", "X-Api-Key": TOKEN}, method=method, ) with urllib.request.urlopen(req) as resp: body = resp.read().decode() print(method, path, resp.status, body) return json.loads(body) if body else None issue = call("POST", f"/workspaces/{WORKSPACE}/projects/{PROJECT}/issues/", {"name": "Webhook test"}) issue_id = issue["id"] call("PATCH", f"/workspaces/{WORKSPACE}/projects/{PROJECT}/issues/{issue_id}/", {"name": "Webhook test updated"}) call("DELETE", f"/workspaces/{WORKSPACE}/projects/{PROJECT}/issues/{issue_id}/") PY -
Inspect the webhook receiver
It prints a payload for the create; nothing arrives for the delete. -
Double-check
webhook_logsdocker compose -f docker-compose-local.yml exec plane-db \ psql -U plane -d plane \ -c "SELECT event_type, request_body->>'action' AS action FROM webhook_logs ORDER BY created_at DESC LIMIT 5;"Output only shows
action = 'created', nodeletedrow.
Environment
Production
Browser
Google Chrome
Variant
Self-hosted
Version
v1.0.0
Hi team! I’ve opened https://github.com/makeplane/plane/pull/8055 to restore DELETE webhooks for issues. The PR reintroduces the missing model_activity.delay() call on the delete endpoint and ensures the webhook task emits a single verb="deleted" payload. After rebuilding on that branch, both the local receiver and webhook_logs table show the expected created + deleted events, so Mattermost/Slack-style integrations get the delete notifications.