Skip to content

Commit 3e32bd6

Browse files
authored
feat: improve workflow error detection and send notification (baserow#5189)
* Support window for error detection * Send a notification when the workflow is disabled
1 parent f1cdb4d commit 3e32bd6

48 files changed

Lines changed: 1893 additions & 119 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
---
2+
name: Create In-App Notification
3+
description: Create or update a Baserow in-app notification for an event. Use when adding a backend `NotificationType`, wiring frontend notification rendering and routing, defining the notification target, or preventing duplicate notifications for the same event object.
4+
---
5+
6+
# Create Baserow In-App Notifications
7+
8+
Use this skill when a task is to add or update an in-app notification shown in Baserow's notification center.
9+
10+
Do not invent a new notification architecture. This repo already has established backend and frontend patterns. Start from the closest existing notification type in the same product area: core, database, builder, automation, integration, premium, or enterprise.
11+
12+
## First Step
13+
14+
Before editing, identify which shape best matches the event:
15+
16+
1. One event sends one notification to one or more explicit users.
17+
2. One event fans out to many users and should be grouped or queued efficiently.
18+
3. One event is instance-wide and should be a broadcast notification.
19+
4. The event should update or reuse an existing notification instead of creating another one.
20+
21+
Then inspect the closest example before editing.
22+
23+
Useful starting points:
24+
25+
- Core notification types: `backend/src/baserow/core/notification_types.py`
26+
- Database notification types: `backend/src/baserow/contrib/database/fields/notification_types.py`
27+
- Premium notification types: `premium/backend/src/baserow_premium/row_comments/notification_types.py`
28+
- Enterprise notification types: `enterprise/backend/src/baserow_enterprise/data_scanner/notification_types.py`
29+
- Backend notification APIs: `backend/src/baserow/core/notifications/handler.py`
30+
- Backend notification base classes: `backend/src/baserow/core/notifications/registries.py`
31+
- Frontend base notification type: `web-frontend/modules/core/notificationTypes.js`
32+
33+
## What A Complete Notification Usually Needs
34+
35+
Most new notifications touch both sides:
36+
37+
1. Backend `NotificationType` subclass and event hook.
38+
2. Backend registration in the relevant app `ready()` method.
39+
3. Frontend `NotificationType` class.
40+
4. Frontend content component used in the notification list.
41+
5. Frontend registration in the relevant `plugin.js`.
42+
6. Targeted backend tests, and frontend tests if the route or rendering logic is non-trivial.
43+
44+
## Backend Pattern
45+
46+
Follow the existing backend shape:
47+
48+
1. Add a typed payload container, usually a dataclass, with the minimal stable fields needed by the UI and routing.
49+
2. Implement a `NotificationType` subclass.
50+
3. Add a helper like `create_notification`, `notify_*`, or `construct_notification`.
51+
4. Call `NotificationHandler.create_direct_notification_for_users(...)` for direct notifications.
52+
5. Use `NotificationHandler.construct_notification(...)` plus `UserNotificationsGrouper` when batching many notifications.
53+
6. Use `NotificationHandler.create_broadcast_notification(...)` only for true broadcast events.
54+
7. Register the notification type in the matching backend app.
55+
56+
Common backend registration points:
57+
58+
- `backend/src/baserow/core/apps.py`
59+
- `backend/src/baserow/contrib/database/apps.py`
60+
- `premium/backend/src/baserow_premium/apps.py`
61+
- `enterprise/backend/src/baserow_enterprise/apps.py`
62+
63+
## Frontend Pattern
64+
65+
If the notification must render inside the app, add the frontend type too:
66+
67+
1. Create a frontend `NotificationType` subclass with the same `type` string.
68+
2. Return the appropriate icon component.
69+
3. Return a content component that renders the notification text.
70+
4. Implement `getRoute(notificationData)` when the notification should be clickable.
71+
5. Register the type in the relevant frontend `plugin.js`.
72+
73+
Common frontend registration points:
74+
75+
- `web-frontend/modules/core/plugin.js`
76+
- `web-frontend/modules/database/plugin.js`
77+
- `premium/web-frontend/modules/baserow_premium/plugin.js`
78+
- `enterprise/web-frontend/modules/baserow_enterprise/plugin.js`
79+
80+
## Define The Target Clearly
81+
82+
Every notification should have a clear target: what object or page the user should land on when they click it.
83+
84+
Prefer storing stable identifiers in `notification.data`, not display-only values. Usually that means IDs plus enough names to render a readable message.
85+
86+
Good target payload examples:
87+
88+
- Row or field event: `database_id`, `table_id`, `row_id`, `field_id`
89+
- Comment event: `comment_id`, `table_id`, `row_id`
90+
- Workspace-scoped event: `workspace_id` or object IDs resolvable within the workspace
91+
- Admin or global event: IDs and query parameters needed for an admin route
92+
93+
Use these rules:
94+
95+
1. Include the smallest set of IDs required to reconstruct the target route.
96+
2. Include names only for display or email text.
97+
3. Keep the target stable even if labels change later.
98+
4. Use `workspace=None` only when the event is truly user-global or instance-global.
99+
5. If the backend email link should point to the same place, keep the backend and frontend route assumptions aligned.
100+
101+
There are two target implementations to consider:
102+
103+
1. Backend `get_web_frontend_url(...)`
104+
Use this when the notification is emailed and should link into the app.
105+
`EmailNotificationTypeMixin` already provides the default `/notification/<workspace_id>/<notification_id>` route when `has_web_frontend_route = True`.
106+
Override it only when the target route cannot be expressed through that default flow.
107+
2. Frontend `getRoute(notificationData)`
108+
Return the real in-app route object based on the IDs stored in `notification.data`.
109+
110+
If the notification redirects through the generic notification route, verify the frontend route can still resolve the final location from the stored data.
111+
112+
## Prevent Duplicate Notifications
113+
114+
Do not blindly create a new notification every time a signal fires. First decide whether repeated events should:
115+
116+
1. Create a new notification every time.
117+
2. Reuse one existing unread notification for the same object.
118+
3. Suppress re-creation while a tracking record still exists.
119+
4. Mark an existing notification as read instead of creating a new one.
120+
121+
The repo uses several duplicate-prevention patterns already:
122+
123+
### Pattern 1: Query for an existing active notification
124+
125+
Use this when the event has a natural unique object, such as an invitation, comment, sync, or scan.
126+
127+
Typical lookup shape:
128+
129+
```python
130+
NotificationHandler.get_notification_by(
131+
user,
132+
notificationrecipient__read=False,
133+
data__contains={"some_object_id": obj.id},
134+
)
135+
```
136+
137+
Use this when you want at most one active notification per recipient and per object. If one already exists, do not create another. Depending on the behavior, you can:
138+
139+
- return early
140+
- update the existing notification data
141+
- mark the existing notification as read as part of a follow-up action
142+
143+
Choose `data` keys that uniquely identify the event target. If multiple objects can share the same notification type, the dedupe key must include all IDs needed to distinguish them.
144+
145+
### Pattern 2: Persist or reuse an event-tracking row
146+
147+
Use this when the same source content may be removed and re-added quickly, and a raw notification query is not enough.
148+
149+
The rich text mention flow uses `RichTextFieldMention` rows to track mention existence and avoid duplicate notifications when content is briefly undone and redone. Follow that approach when the event source has lifecycle state that should outlive a single signal call.
150+
151+
### Pattern 3: Group creation before writing recipients
152+
153+
Use `UserNotificationsGrouper` when one operation can generate many notifications across many users. This reduces fan-out overhead and avoids ad hoc per-user creation loops.
154+
155+
### Pattern 4: Update or mark read instead of inserting
156+
157+
If the event resolves a prior notification, prefer updating state over inserting another notification. For example, invitation follow-up flows mark the original invitation notification as read.
158+
159+
## Choosing A Dedupe Key
160+
161+
A dedupe key is usually an implicit tuple made from:
162+
163+
1. notification `type`
164+
2. recipient user
165+
3. active state, usually unread and uncleared
166+
4. one or more stable object IDs stored in `data`
167+
168+
Examples:
169+
170+
- One notification per invitation per user:
171+
`type + recipient + data.invitation_id`
172+
- One notification per row comment mention per user:
173+
`type + recipient + data.comment_id`
174+
- One notification per row-field mention per user:
175+
`type + recipient + data.field_id + data.row_id`
176+
177+
Do not dedupe on mutable names or message text.
178+
179+
## Implementation Checklist
180+
181+
When adding a new notification, verify all of these:
182+
183+
1. The `type` string is unique and stable.
184+
2. The payload contains stable target IDs.
185+
3. The workspace is correct for permission-scoped listing.
186+
4. The sender is correct, or `None` if there is no meaningful sender.
187+
5. Duplicate creation behavior is explicit.
188+
6. Backend registration is present.
189+
7. Frontend registration is present if the notification appears in-app.
190+
8. The route works for the intended target.
191+
9. Tests cover both creation and duplicate prevention behavior.
192+
193+
## Testing Expectations
194+
195+
Add the narrowest backend tests that prove:
196+
197+
1. The right recipients are selected.
198+
2. The notification payload contains the target IDs.
199+
3. The notification is workspace-scoped correctly.
200+
4. A duplicate event does not create an extra active notification when dedupe is required.
201+
5. The route-related data needed by the frontend is present.
202+
203+
Useful existing tests:
204+
205+
- `premium/backend/tests/baserow_premium_tests/row_comments/test_row_comments_notification_types.py`
206+
- `enterprise/backend/tests/baserow_enterprise_tests/data_scanner/test_data_scanner_notification_types.py`
207+
208+
If you add custom frontend routing or rendering logic, add or update a focused frontend unit test near the notification type or component.
209+
210+
## Search Patterns
211+
212+
Use these searches to move quickly:
213+
214+
- `rg -n "class .*NotificationType" backend/src premium/backend/src enterprise/backend/src`
215+
- `rg -n "notification_type_registry.register" backend/src premium/backend/src enterprise/backend/src`
216+
- `rg -n "new .*NotificationType\\(context\\)" web-frontend premium/web-frontend enterprise/web-frontend`
217+
- `rg -n "NotificationHandler\\.create_direct_notification_for_users|UserNotificationsGrouper|create_broadcast_notification" backend/src premium/backend/src enterprise/backend/src`
218+
- `rg -n "data__contains=.*_id|get_notification_by\\(" backend/src premium/backend/src enterprise/backend/src`
219+
220+
## Guardrails
221+
222+
- Do not create a new notification type without checking whether an existing one should be reused or updated.
223+
- Do not store only display text if the notification needs to link back to an object.
224+
- Do not dedupe on mutable fields like names or messages.
225+
- Do not use broadcasts for ordinary per-user events.
226+
- Do not skip frontend registration when the notification must render in-app.
227+
- Do not create duplicate unread notifications for the same object unless that is explicitly the desired product behavior.

.editorconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ insert_final_newline = true
1111
[Makefile]
1212
indent_style = tab
1313

14-
[*.{js,mjs,yml,scss,eslintrc,stylelintrc,vue,html,json,ts,prettierrc}]
14+
[*.{js,jsx,mjs,yml,scss,eslintrc,stylelintrc,vue,html,json,ts,tsx,prettierrc}]
1515
indent_size = 2
1616

1717
[*.md]

AGENTS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ For direct package-manager use, backend commands run through `uv` and frontend c
2121

2222
Python targets Python 3.14, uses 4-space indentation, and is formatted and linted with Ruff (`ruff check`, `ruff format`) with an 88-character line length. Follow existing Django app/module naming and keep new tests in `test_*.py` or `*_test.py` files. Frontend code uses ESLint, Stylelint, and Prettier; SCSS should follow BEM-style naming already used in `web-frontend/modules`.
2323

24+
## Technology Stack
25+
26+
Backend code uses Django, Django REST Framework, Celery, PostgreSQL, Redis, and pytest/pytest-django. Python dependencies are managed with `uv`.
27+
28+
Frontend code uses Vue 3, Nuxt 3, Vuex, Vite, Vitest, Storybook, SCSS, ESLint, Stylelint, Prettier, and `yarn`. Render functions must use Vue 3 semantics, for example importing `h` from `vue` instead of expecting `render(h)` to receive it. JSX-bearing frontend files must use a `.jsx` or `.tsx` extension so Vite can parse them.
29+
2430
## Testing Guidelines
2531

2632
Backend tests use `pytest` with `pytest-django`; frontend tests use `vitest`; browser flows live in `e2e-tests/`. Add unit tests for backend changes and targeted frontend tests for component or store behavior.
@@ -40,6 +46,7 @@ Reusable skills live in `.agents/skills/`. Each subdirectory is a self-contained
4046
| `add-django-config-env-var` | Adding a new Django setting backed by an env var and propagating it to `base.py`, docker-compose files, `env-remap.mjs`, and `docs/installation/configuration.md` |
4147
| `write-frontend-unit-test` | Writing or fixing frontend unit tests in `web-frontend`, `premium/web-frontend`, or `enterprise/web-frontend` |
4248
| `create-update-service` | Creating or updating an integration type or service type in `contrib/integrations` |
49+
| `create-in-app-notification` | Creating or updating a Baserow in-app notification for an event, including backend and frontend registration, target routing data, and duplicate-prevention behavior |
4350

4451
## Security & Configuration Tips
4552

backend/src/baserow/config/settings/base.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -857,6 +857,9 @@ def __setitem__(self, key, value):
857857
_automation_workflow_rate_limits_env = os.getenv(
858858
"BASEROW_AUTOMATION_WORKFLOW_RATE_LIMITS"
859859
)
860+
_automation_workflow_error_limits_env = os.getenv(
861+
"BASEROW_AUTOMATION_WORKFLOW_ERROR_LIMITS"
862+
)
860863

861864
if _automation_workflow_rate_limits_env is not None:
862865
_automation_workflow_rate_limit_values = [
@@ -894,6 +897,28 @@ def __setitem__(self, key, value):
894897
_legacy_workflow_rate_limit_window_seconds or 5,
895898
)
896899
)
900+
if _automation_workflow_error_limits_env is not None:
901+
_automation_workflow_error_limit_values = [
902+
int(value.strip())
903+
for value in _automation_workflow_error_limits_env.split(",")
904+
if value.strip()
905+
]
906+
else:
907+
_automation_workflow_error_limit_values = [20, 300]
908+
909+
if len(_automation_workflow_error_limit_values) % 2 != 0:
910+
raise ImproperlyConfigured(
911+
"BASEROW_AUTOMATION_WORKFLOW_ERROR_LIMITS must contain an even number of "
912+
"comma-separated integers formatted as max_errors,window_seconds pairs."
913+
)
914+
915+
AUTOMATION_WORKFLOW_ERROR_LIMITS = tuple(
916+
(
917+
_automation_workflow_error_limit_values[index],
918+
_automation_workflow_error_limit_values[index + 1],
919+
)
920+
for index in range(0, len(_automation_workflow_error_limit_values), 2)
921+
)
897922
AUTOMATION_WORKFLOW_MAX_CONSECUTIVE_ERRORS = int(
898923
os.getenv("BASEROW_AUTOMATION_WORKFLOW_MAX_CONSECUTIVE_ERRORS", 5)
899924
)

backend/src/baserow/contrib/automation/api/workflows/errors.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
"The workflow id {e.workflow_id} does not belong to the automation.",
1313
)
1414

15+
ERROR_AUTOMATION_WORKFLOW_NOTIFICATION_RECIPIENTS_INVALID = (
16+
"ERROR_AUTOMATION_WORKFLOW_NOTIFICATION_RECIPIENTS_INVALID",
17+
HTTP_400_BAD_REQUEST,
18+
"{e}",
19+
)
20+
1521
ERROR_AUTOMATION_NODE_DOES_NOT_EXIST = (
1622
"ERROR_AUTOMATION_NODE_DOES_NOT_EXIST",
1723
HTTP_404_NOT_FOUND,

backend/src/baserow/contrib/automation/api/workflows/serializers.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
class AutomationWorkflowSerializer(serializers.ModelSerializer):
1717
published_on = serializers.SerializerMethodField()
1818
state = serializers.SerializerMethodField()
19+
notification_recipient_ids = serializers.SerializerMethodField()
1920

2021
class Meta:
2122
model = AutomationWorkflow
@@ -29,6 +30,7 @@ class Meta:
2930
"published_on",
3031
"state",
3132
"graph",
33+
"notification_recipient_ids",
3234
)
3335
extra_kwargs = {
3436
"id": {"read_only": True},
@@ -47,6 +49,14 @@ def get_state(self, obj):
4749
published_workflow = AutomationWorkflowHandler().get_published_workflow(obj)
4850
return published_workflow.state if published_workflow else WorkflowState.DRAFT
4951

52+
@extend_schema_field(serializers.ListField(child=serializers.IntegerField()))
53+
def get_notification_recipient_ids(self, obj):
54+
"""
55+
Use the prefetched recipients.
56+
"""
57+
58+
return sorted((recipient.id for recipient in obj.notification_recipients.all()))
59+
5060

5161
class CreateAutomationWorkflowSerializer(serializers.ModelSerializer):
5262
class Meta:
@@ -62,10 +72,23 @@ class UpdateAutomationWorkflowSerializer(serializers.ModelSerializer):
6272
f"{ALLOW_TEST_RUN_MINUTES} minutes."
6373
),
6474
)
75+
notification_recipient_ids = serializers.ListField(
76+
child=serializers.IntegerField(),
77+
required=False,
78+
help_text=(
79+
"The user IDs of the workspace members that should receive "
80+
"notifications related to this workflow."
81+
),
82+
)
6583

6684
class Meta:
6785
model = AutomationWorkflow
68-
fields = ("name", "allow_test_run", "state")
86+
fields = (
87+
"name",
88+
"allow_test_run",
89+
"state",
90+
"notification_recipient_ids",
91+
)
6992
extra_kwargs = {
7093
"name": {"required": False},
7194
}

0 commit comments

Comments
 (0)