Skip to content

fix(api): personal-tasks endpoints must fire model_activity (Lark bot fan-out)#12

Merged
JOBYINC merged 1 commit into
feature/lark-oauth-providerfrom
fix/personal-tasks-webhook-notification
May 20, 2026
Merged

fix(api): personal-tasks endpoints must fire model_activity (Lark bot fan-out)#12
JOBYINC merged 1 commit into
feature/lark-oauth-providerfrom
fix/personal-tasks-webhook-notification

Conversation

@JOBYINC
Copy link
Copy Markdown
Owner

@JOBYINC JOBYINC commented May 20, 2026

Summary

  • PersonalTaskAPIEndpoint.post() and .patch() now fire model_activity.delay(...) so webhook subscribers (Lark bot, et al.) receive issue.created / issue.updated events for system-token-created tasks. Previously only issue_activity.delay fired, which is why Tom-created tasks landed in the target user's My Tasks bucket but never DM'd them on Lark.
  • PATCH snapshots the pre-update serialized issue (current_instance) so model_activity can diff old vs new and emit per-field webhook_activity events — matching the standard IssueDetailAPIEndpoint.patch() pattern (issue.py:785).
  • 409 idempotent-reuse path stays silent — nothing was actually created, so refiring the webhook would be wrong (verified by a dedicated test).

Why

Regression vs. the standard POST /api/v1/workspaces/{slug}/projects/{pid}/work-items/ path: that endpoint fires both issue_activity and model_activity (issue.py:473, :484). The new system-token personal-tasks endpoint only mirrored half the pattern. model_activity is the entry point for webhook_activity.delay, which is what feeds the Lark-bot subscriber that DMs assignees.

Files

  • apps/api/plane/api/views/personal_task.py — import model_activity, fire it in post() (current_instance=None) and patch() (snapshot pre-update via IssueSerializer(issue).data).
  • apps/api/plane/tests/contract/api/test_personal_tasks.py — 3 new tests in TestPersonalTasksWebhookFanOut: POST fan-out, PATCH fan-out, 409 no-refire guard. Autouse _no_celery fixture also patches the new task entry point.

Test plan

  • pytest plane/tests/contract/api/test_personal_tasks.py -v → 14 passed (11 existing + 3 new).
  • Once merged + lark-stable image rebuilt + redeployed: Tom creates a task on behalf of a target user via POST /personal-tasks/, assignee receives the Lark DM card.
  • PATCH with matching external_source updates a personal task; assignee receives Lark "updated" notification.
  • Duplicate POST returns 409 and no second Lark notification fires.

Related

…ot fan-out)

PersonalTaskAPIEndpoint.post() and .patch() previously fired only
issue_activity.delay, missing the model_activity.delay call that
IssueListCreateAPIEndpoint / IssueDetailAPIEndpoint both make. That
silently dropped webhook fan-out, so the Lark bot never received the
issue.created / issue.updated event and never DM'd the assignee for
tasks created via the system-token path.

POST now fires model_activity with current_instance=None; PATCH
snapshots the pre-update serialized issue so per-field updated events
can be diffed. The 409 idempotent-reuse path stays silent — nothing
was created.

Adds 3 contract tests covering POST fan-out, PATCH fan-out, and the
409 no-refire guard, plus extends the autouse _no_celery fixture to
patch the new task entry point.
@JOBYINC JOBYINC merged commit 32a7c8e into feature/lark-oauth-provider May 20, 2026
JOBYINC added a commit that referenced this pull request May 20, 2026
…l-task create (#13)

The actual Lark notification path is NOT model_activity / webhook
fan-out (PR #12 was based on a wrong hypothesis). Lark DMs are
dispatched inline from `issue_activity` itself: after IssueActivity
rows are bulk_create'd, `dispatch_lark_for_activities` walks the rows
and fires `notify_issue_assigned_task.delay` for any row with
`field="assignees"` + `new_identifier=<user>`.

That assignee row is only created when `create_issue_activity` calls
`track_assignees`, and the gate at issue_activities_task.py:581 is:

    if requested_data.get("assignee_ids") is not None: ...

The plane/api IssueSerializer uses `assignees` as its write key (vs.
the plane/app variant which uses `assignee_ids`). personal_task.py
forwards the request body as-is, so the gate never fires for the
system-token create path → no assignee IssueActivity row → no
Lark DM, even though `LARK_NOTIFICATIONS_ENABLED=1` and the worker
correctly received the issue_activity task (verified in prod logs).

Fix: mirror `payload["assignees"]` to `payload["assignee_ids"]` in
the dict passed to `issue_activity.delay`. The serializer.save()
above still consumes `assignees` (the serializer key it knows about);
only the activity tracker payload is augmented.

Adds a contract test that asserts the requested_data sent to
issue_activity includes `assignee_ids` matching the resolved owner —
the precise contract the gate at line 581 needs.

PATCH path is unaffected: `update_issue_activity` ATTRIBUTE_MAPPER
already maps both `"assignees"` and `"assignee_ids"` to track_assignees.

Co-authored-by: Marcus Cheung <marcusm5@Marcuss-MacBook-Pro.local>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant