Skip to content

fix(api): mirror assignees β†’ assignee_ids so Lark DM actually fires#13

Merged
JOBYINC merged 1 commit into
feature/lark-oauth-providerfrom
fix/personal-tasks-assignee-ids-alias
May 20, 2026
Merged

fix(api): mirror assignees β†’ assignee_ids so Lark DM actually fires#13
JOBYINC merged 1 commit into
feature/lark-oauth-providerfrom
fix/personal-tasks-assignee-ids-alias

Conversation

@JOBYINC
Copy link
Copy Markdown
Owner

@JOBYINC JOBYINC commented May 20, 2026

Summary

PR #12 was based on a wrong hypothesis (the task brief said "strongly suspect model_activity"; that was wrong). Real Lark fan-out path:

issue_activity.delay(…)
  β†’ create_issue_activity()                # issue_activities_task.py:557
    β†’ if requested_data.get("assignee_ids") is not None:  # line 581 β€” THE GATE
        track_assignees()                  # writes IssueActivity(field="assignees")
  β†’ bulk_create(issue_activities)
  β†’ dispatch_lark_for_activities(rows)     # lark_notify_task.py:44
    β†’ notify_issue_assigned_task.delay(…)  # the Lark DM

The plane/api/serializers/issue.IssueSerializer uses assignees as its write key. The plane/app/serializers/issue.IssueCreateSerializer (used by the web UI) uses assignee_ids. The activity tracker's gate hard-codes assignee_ids, so the system-token create path never triggers track_assignees and never produces an assignee row for the Lark dispatcher to fan out.

Fix (3 lines): when building the payload for issue_activity.delay(...), mirror assignees into assignee_ids so the gate fires. The serializer.save() above is untouched β€” it still consumes the original assignees key.

Evidence from prod (before this fix)

LARK_NOTIFICATIONS_ENABLED=1 is set in both plane-api-1 and plane-worker-1 containers. Worker logs show issue_activity task received with the correct issue.activity.created payload β€” but requested_data only has \"assignees\": […], not \"assignee_ids\":

{\"type\": \"issue.activity.created\",
 \"requested_data\": \"{…, \\\"assignees\\\": [\\\"92184…\\\"]}\",  ← gate at line 581 misses
 …}

No notify_issue_assigned_task is fired downstream.

Why this matters

Files

  • apps/api/plane/api/views/personal_task.py β€” alias assignees β†’ assignee_ids in the activity_payload passed to issue_activity.delay. Comment explains the gate.
  • apps/api/plane/tests/contract/api/test_personal_tasks.py β€” adds test_post_requested_data_includes_assignee_ids_for_lark_gate which asserts the precise contract the gate needs.

Test plan

  • pytest plane/tests/contract/api/test_personal_tasks.py -v β†’ 15 passed (was 14 before round 2).
  • Confirmed RED before fix: assertion fails with payload showing only assignees key.
  • After merge + rebuild + redeploy: Tom POSTs a personal task β†’ assignee receives Lark DM with card_issue_assigned.
  • PATCH still works (existing test covers; PATCH path already maps both keys via ATTRIBUTE_MAPPER).

…l-task create

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.
@JOBYINC JOBYINC merged commit 95765f8 into feature/lark-oauth-provider May 20, 2026
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