makeplane / makeplane/plane

[bug]: Issue deletion webhooks never fire (create/update work)

Open
#8,054 5 comments 0 reactions 1 assignee View on GitHub

@dheeru0198 is already working on this.

Since Nov 4, 2025.

🐛bug
Dominant language
TypeScript
Stars
59.6k
Forks
5.8k
Avg merge
1d 22h
Merged PRs (30d)
49

Description

Is there an existing issue for this?
  • 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
  1. 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
    
  2. Seed a minimal workspace/project/webhook (inside api container)

    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 = "admin@example.com", "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)
    PY
    

    Note the API_TOKEN, WORKSPACE_SLUG, and PROJECT_ID printed here.

  3. 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
    
  4. 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
    
  5. Inspect the webhook receiver
    It prints a payload for the create; nothing arrives for the delete.

  6. Double-check webhook_logs

    docker 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', no deleted row.

Environment

Production

Browser

Google Chrome

Variant

Self-hosted

Version

v1.0.0

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.