Skip to content

Commit 7a2c1ca

Browse files
committed
feat(app-shell): Studio flow start node offers a "Record created or updated" trigger (#3427)
The record-change trigger now supports record-after-write (create OR update in one flow), so the flow designer's start-node trigger picker offers a "Record created or updated" option. Selecting it shows the Object and Entry-condition fields, and the scope resolver puts both `record` and `previous` in scope for it (`previous == null` is how an author branches the create leg) — mirroring the runtime binding that fires the flow on both insert and update. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018939yJ413zG3irLzcTtqaa
1 parent 1a03af6 commit 7a2c1ca

6 files changed

Lines changed: 42 additions & 4 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@object-ui/app-shell": patch
3+
---
4+
5+
feat(app-shell): Studio flow start node offers a "Record created or updated" trigger (#3427)
6+
7+
The record-change trigger now supports `record-after-write` (create OR update in
8+
one flow), so the flow designer's start-node trigger picker offers a "Record
9+
created or updated" option. Selecting it shows the Object and Entry-condition
10+
fields, and the scope resolver puts both `record` and `previous` in scope for it
11+
(`previous == null` is how an author branches the create leg) — mirroring the
12+
runtime binding that fires the flow on both insert and update.

packages/app-shell/src/views/metadata-admin/i18n.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3210,6 +3210,7 @@ const FLOW_FIELD_ZH: Record<string, Record<string, FlowFieldZh>> = {
32103210
opts: {
32113211
'record-after-create': '记录创建后',
32123212
'record-after-update': '记录更新后',
3213+
'record-after-write': '记录创建或更新后',
32133214
'record-before-update': '记录更新前',
32143215
'record-after-delete': '记录删除后',
32153216
'record-change': '记录变更(任意)',

packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ describe('start node trigger-field gating (#5)', () => {
2020
expect(isFieldVisible(condition, node, fields)).toBe(true);
2121
});
2222

23+
it('offers a "created or updated" (record-after-write) option and shows the record fields for it (#3427)', () => {
24+
const triggerType = fields.find((f) => f.id === 'triggerType')!;
25+
expect(triggerType.options?.some((o) => o.value === 'record-after-write')).toBe(true);
26+
const node = { id: 'start', type: 'start', config: { triggerType: 'record-after-write' } };
27+
expect(isFieldVisible(objectName, node, fields)).toBe(true);
28+
expect(isFieldVisible(condition, node, fields)).toBe(true);
29+
});
30+
2331
it('shows for a schedule trigger too', () => {
2432
const node = { id: 'start', type: 'start', config: { triggerType: 'schedule' } };
2533
expect(isFieldVisible(objectName, node, fields)).toBe(true);

packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ const FLOW_NODE_CONFIG: Record<string, FlowConfigField[]> = {
247247
options: [
248248
{ value: 'record-after-create', label: 'Record created' },
249249
{ value: 'record-after-update', label: 'Record updated' },
250+
{ value: 'record-after-write', label: 'Record created or updated' },
250251
{ value: 'record-before-update', label: 'Record before update' },
251252
{ value: 'record-after-delete', label: 'Record deleted' },
252253
{ value: 'record-change', label: 'Record changed (any)' },
@@ -261,12 +262,12 @@ const FLOW_NODE_CONFIG: Record<string, FlowConfigField[]> = {
261262
ref: { kind: 'object' },
262263
placeholder: 'crm_lead',
263264
help: 'Target object for record / scheduled-scan triggers.',
264-
showWhen: { field: 'triggerType', equals: ['record-after-create', 'record-after-update', 'record-before-update', 'record-after-delete', 'record-change', 'schedule', 'webhook', 'event'] },
265+
showWhen: { field: 'triggerType', equals: ['record-after-create', 'record-after-update', 'record-after-write', 'record-before-update', 'record-after-delete', 'record-change', 'schedule', 'webhook', 'event'] },
265266
}),
266267
cfg('condition', 'Entry condition', 'expression', {
267268
placeholder: 'status == "qualifying" && previous.status != "qualifying"',
268-
help: 'CEL predicate — the flow runs only when this is true (for time-relative sweeps it gates each matched record). Leave empty to run on every event.',
269-
showWhen: { field: 'triggerType', equals: ['record-after-create', 'record-after-update', 'record-before-update', 'record-after-delete', 'record-change', 'schedule', 'time_relative', 'webhook', 'event'] },
269+
help: 'CEL predicate — the flow runs only when this is true (for time-relative sweeps it gates each matched record). Leave empty to run on every event. On a "created or updated" trigger, `previous == null` selects the create path.',
270+
showWhen: { field: 'triggerType', equals: ['record-after-create', 'record-after-update', 'record-after-write', 'record-before-update', 'record-after-delete', 'record-change', 'schedule', 'time_relative', 'webhook', 'event'] },
270271
}),
271272
// Schedule descriptor — author the canonical nested `config.schedule` object
272273
// the runtime actually reads (resolveTriggerBinding → normalizeSchedule). This

packages/app-shell/src/views/metadata-admin/inspectors/flow-scope.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,16 @@ describe('resolveFlowScope — graph-aware in-scope references', () => {
133133
expect(groupTokens(resolveFlowScope(create, 'decide'), 'trigger')).not.toContain('previous');
134134
});
135135

136+
it('offers `record` and `previous` for a create-or-update (record-after-write) trigger (#3427)', () => {
137+
// A write trigger fires on update too, so `previous` must be offered — it is
138+
// how an author branches create vs update (`previous == null`).
139+
const write = { ...draft, nodes: [{ id: 'start', type: 'start', config: { triggerType: 'record-after-write', objectName: 'crm_lead' } }, ...draft.nodes.slice(1)] };
140+
const scope = resolveFlowScope(write, 'decide');
141+
expect(groupTokens(scope, 'trigger')).toContain('record');
142+
expect(groupTokens(scope, 'trigger')).toContain('previous');
143+
expect(scope.trigger).toEqual({ objectName: 'crm_lead', fieldPrefix: 'record.', includePrevious: true });
144+
});
145+
136146
it('de-dupes a name that is both a declared variable and an upstream output', () => {
137147
// `lead_score` is declared AND assigned upstream — it should appear once.
138148
const scope = resolveFlowScope(draft, 'decide');

packages/app-shell/src/views/metadata-admin/inspectors/flow-scope.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,20 @@ interface FlowEdgeLike {
8282
const RECORD_TRIGGER_TYPES = new Set([
8383
'record-after-create',
8484
'record-after-update',
85+
'record-after-write', // create OR update (#3427)
86+
'record-before-write',
8587
'record-before-update',
8688
'record-after-delete',
8789
'record-change',
8890
]);
89-
/** Trigger types that carry a meaningful `previous` snapshot of the record. */
91+
/** Trigger types that carry a meaningful `previous` snapshot of the record.
92+
* `record-*-write` fires on update too, so `previous` is offered (empty on the
93+
* create leg — `previous == null` is how authors branch on which happened). */
9094
const PREVIOUS_TRIGGER_TYPES = new Set([
9195
'record-after-update',
9296
'record-before-update',
97+
'record-after-write',
98+
'record-before-write',
9399
'record-change',
94100
]);
95101

0 commit comments

Comments
 (0)