A NocoBase user reported a bug on the official forum in September (t/14308, in Chinese). In my translation, the gist is:
A has a many-to-many relation to B. B has "after create" and "after update" workflows. When I add or edit B rows directly, the workflows run. When I add or edit them from A's form, through the sub-table, they don't. These are ordinary inserts and updates, so shouldn't they trigger?
A staff member answered the next day: this is a current product limitation, not a configuration problem. Collection events only watch changes to their own collection. Rows submitted together with the parent through a sub-table are a nested write of association data, so the child's collection event does not fire. Editing the child directly in its own block — through the child table's own API — does. If you need to process children after the parent is saved, have the parent's workflow loop over them and write them with create and update nodes; data operations inside the app like these trigger the child's events.
The documentation says the same thing in a note titled "Triggering by Bulk Data Operations is Not Currently Supported", with an example: create an article together with several tags, and only the article's workflow runs.
The word "bulk" is where I got curious. Posting an array to create is a bulk operation too. So is an update with a filter, or an XLSX import. Do those trigger? And does the workaround — writing from a workflow node — actually trigger the child's workflow with default settings?
So I set up 17 write paths and counted how many workflow executions each one produced.
Test setup: NocoBase 2.1.39 and 2.2.21 (official Docker images) + PostgreSQL 16. Same script on both; all 17 paths gave the same result (also on 2.2.17, 2.2.18 and 2.2.20 along the way). 2.1.39 was a fresh install; 2.2.21 is an instance created on 2.2.17 and upgraded. Source links point at the
v2.2.21tag. At the time of writing,latestis 2.2.26, and none of the files I cite changed between 2.2.21 and 2.2.26.
The short version
| How the child row is written | Child's workflow |
|---|---|
REST create / update, one row |
runs |
REST create with an array (2 rows) |
runs once per row |
REST update with a filter (2 rows) |
runs once per row |
| XLSX import (2 rows) | runs once per row |
| Nested in the parent's create/update (one-to-many create and update, many-to-many create) | does not run |
Relation resource (orders/<id>/items:create etc.) |
runs |
| Workflow "Create record" node | runs |
| Workflow "Update record" node, "Update one by one" | runs |
| Workflow "Update record" node, "Update in a batch" — the default when you build it in the UI | does not run |
| SQL node, or writing to the database directly | does not run |
| (for reference) "Update record" node created via API without an update mode | runs |
"Bulk" over REST is fine: each row gets its own execution. What does not trigger the child's workflow is the nested write, the batch mode of the update node, and SQL.
The second one is the trap. It is the default.
How I measured
Three collections:
| Collection | Relations |
|---|---|
orders (parent) |
one-to-many to items, many-to-many to tags
|
items (child) |
|
tags (child) |
items got an "after create" and an "after update" workflow, tags an "after create" one, all asynchronous. For each path I wrote the rows, then counted how many new rows appeared in executions for those workflows.
A zero only means something if the write actually happened, so for every path that produced zero executions I checked the database: the rows were there, and the values had changed. For paths that did trigger, I checked that context.data.title in each execution matched the value that path wrote.
REST "bulk" triggers once per row
| Path | Rows written | Executions |
|---|---|---|
items:create, one row |
1 | 1 |
items:update, one row |
1 | 1 |
items:create with an array |
2 | 2 |
items:update?filter=… |
2 | 2 |
items:importXlsx |
2 | 2 |
So whatever the documentation's "bulk data operations" covers, these behave differently. Array creates and filtered updates go through the repository one row at a time and emit an event for each row. The XLSX importer inserts the rows with bulkCreate, then emits afterCreateWithAssociations for each inserted row itself (xlsx-importer.ts L566).
Nested writes do not trigger
This is the forum's case: the child rows travel inside the parent's request.
POST /api/orders:create?updateAssociationValues[]=items
{ "title": "order A", "items": [{ "title": "nested 1" }, { "title": "nested 2" }] }
| Path | Child rows | Executions |
|---|---|---|
orders:create with 2 items (one-to-many) |
2 created | 0 |
orders:update changing 2 items (one-to-many) |
both changed | 0 |
orders:create with 2 tags (many-to-many) |
2 created | 0 |
The rows exist and the values changed. The child's workflow never ran.
The UI does the same thing. I put items on the orders create form as a sub-table, entered two rows, and submitted.
The orders create form with two rows in the items sub-table
The request the browser sent was POST /api/orders:create?updateAssociationValues[]=items (from the app log) — the same shape as the first row of the table. Both children were created under the new order, and the items "after create" workflow got no new executions.
Why
Collection events listen for two events, afterCreateWithAssociations and afterUpdateWithAssociations (CollectionTrigger.ts L43–44). For the create, update and relation-resource writes in this test, these are the places that emit them:
| Emitted by | For which collection |
|---|---|
repository.ts L712 / L820 (create / update) |
the collection being written |
relation-repository.ts L201 (create via relation resource) |
the target (child) collection |
multiple-relation-repository.ts L180 (update via relation resource) |
the target (child) collection |
orders:create goes through repository.ts, so orders gets its event. The nested children are written by update-associations.ts: new ones through Sequelize's association method (model[createAccessor], L545), existing ones through instance.update (L594). Neither emits the event.
The relation resource does trigger
Writing the child through the parent's relation resource is a different path:
POST /api/orders/<id>/items:create
POST /api/orders/<id>/items:update?filterByTk=<child id>
Both produced one execution each, because they go through relation-repository.ts and multiple-relation-repository.ts, which emit the event on the child collection.
The staff answer says editing in the child's own block uses "the child table's own API". In my runs, both items:create and the relation resource triggered. I did not check which of the two a child block in the UI actually calls.
The workaround, and its default
The suggested workaround is to write the children from the parent's workflow, and the answer says those writes trigger the child's events. I tested the two nodes you would use.
| Node | Executions |
|---|---|
| "Create record" | 1 |
"Update record", "Update one by one" (individualHooks: true) |
1 |
"Update record", "Update in a batch" (individualHooks: false) |
0 (the row did change) |
The update node has an "Update mode" setting, and the two modes take different paths in repository.ts. "Update in a batch" collects the primary keys of the matching rows and runs a single UPDATE over them, then returns before any event is emitted. "Update one by one" updates each row and emits an event for each.
When you add an "Update record" node in the UI, "Update in a batch" is selected.
The "Update record" node settings, right after adding it
Right after you add the node, its stored config is just {"collection": "items"} — there is no individualHooks yet. The settings form fills in false when it is missing (update.tsx L59), and saving stores "individualHooks": false.
To be fair to the UI, it does say so. The tooltip on "Update in a batch" reads, in the source:
Update all eligible data at one time, which has better performance when the amount of data is large. But association fields are not supported (unless foreign key in current collection), and the updated data will not trigger other workflows.
But that is a tooltip on the default option. If you follow the forum's workaround with the defaults, the child's values change and the child's "after update" workflow does not run. Switch the node to "Update one by one".
One more wrinkle. A node created through the API without individualHooks goes down the one-by-one path and does trigger (1 execution). So the same node behaves differently depending on whether it was built in the UI or via API. If you create workflows programmatically, set individualHooks explicitly.
SQL does not trigger
| Path | Write | Executions |
|---|---|---|
SQL node, insert
|
row inserted | 0 |
SQL node, update
|
value changed | 0 |
insert through psql |
row inserted | 0 |
This one is in the documentation and holds. The SQL node passes the statement to sequelize.query (SQLInstruction.ts L73) and never touches the repository, so no event is emitted.
What I did not test
- UI operations other than the sub-table create form and the update node settings. Everything else was written through the API. I did not check which API a child block calls when you add or edit a row in it.
- Many-to-many updates and deletes. For many-to-many I only tested nested create.
- Synchronous workflows. All of mine were asynchronous.
- MySQL. PostgreSQL only.
So
On 2.1.39 and 2.2.21, a child's collection-event workflow does not run when the child is written nested inside its parent (which is what a sub-table does), when it is written by an update node in batch mode (the UI default), or when it is written by SQL. REST arrays, filtered updates and imports trigger once per row.
If the forum's workaround is what you reach for, check the update node's mode before you trust it.
(Measured on NocoBase 2.1.39 and 2.2.21 with PostgreSQL 16, October 2026. Behavior changes between versions; check yours.)
Related: Are NocoBase 2.x workflows really sequential? · Ten NocoBase workflows, one increment · NocoBase updates by primary key, not by your filter


Top comments (0)