Skip to content

Commit 4548ba3

Browse files
feat(spec,automation,app-shell): a read-only record_link screen field shows a record the run created, by its display name, as a link to its record page (#26355)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9VyLmRWa1EMU9YudB6tnf1C)_ Fixes #26304 Fixes #25872 Clause-②: yes (widening) Metadata-protocol: added ## Summary A screen flow that creates records can now end on a result screen that links them. This is ruling B on #25872 (6422899013) and the contract card #26304, landed in one PR across spec, engine and console, in the shape of the `file` screen field (#26134). ```ts { id: 'done', type: 'screen', config: { title: 'Lead converted', fields: [ { name: 'account_link', label: 'Account', type: 'record_link', reference: 'crm_account', defaultValue: { dialect: 'cel', source: 'account.id' } }, { name: 'contact_link', label: 'Contact', type: 'record_link', reference: 'crm_contact', defaultValue: { dialect: 'cel', source: 'contact.id' } }, ] } }, ``` - **Spec.** `ScreenFieldConfigSchema` gains the `type` member `'record_link'`. `reference` and `defaultValue` are required on it, and `required`, `options`, `placeholder`, `min` and `max` are refused, each at its own key. Every other `type` value parses exactly as before. - **Engine.** `validateScreenInputs` refuses any value sent under a `record_link` field's name (`null` included, visible or hidden) with `invalid_value` inside `400 INVALID_SCREEN_INPUT`. The value is never bound to a flow variable, and the refusal keeps the pause. `judgeHeadlessScreen` never counts a `record_link` field as answered by a caller, so a result screen always pauses. - **Console.** `ScreenFieldInput` gets a `record_link` branch. It shows the record's display name through `LookupField`'s `readonly` branch (which fetches the record and names it from the referenced object's schema, ADR-0079 `nameField`), inside a router `Link` to `/apps/APP/OBJECT/record/ID` built by `appRecordPath`, the builder the record page's related lists use. An empty id draws the platform's "—" with no link. An object the console cannot route to, or a mount with no router, shows the name without a link. `visibleWhen` works as on any field. `initialScreenValues` never seeds the field and `screenResumeValues` strips it, so the dialog never posts it. - **Docs.** `content/docs/automation/flows.mdx` gains "A read-only record link", and the resume-contract list names the new refusal. The designer form's `reference` column now reads "Target object" and describes both `lookup` and `record_link`. ## The member's name: `record_link`, not `record` The ruling let the dev propose the name and suggested `record`. A screen field's `type` borrows the object field vocabulary (`text`, `number`, `lookup`, `file`, and so on), and `FieldType` already has `'record'` for a name-keyed map of embedded sub-objects (`field.zod.ts`, ADR-0007). One word with two meanings in one field vocabulary is a trap for an author, so the member is `record_link`, snake_case like `master_detail`. The name lives in one exported constant, `SCREEN_FIELD_RECORD_LINK_TYPE`, which the engine and the console both import. ## For the contract review: final shape and refusal texts Shape: no new key. `type` stays `z.string().optional()`. The rule is a branch of the existing `superRefine` on `ScreenFieldConfigSchema`, taken when `type === 'record_link'` (exact match). It adds one issue per broken rule, each with `code: 'custom'`, and returns before the lookup rule. `.shape` still lists the same keys. | Path | When | Message | |:--|:--|:--| | `reference` | absent, empty or blank | `SCREEN_FIELD_RECORD_LINK_REFERENCE_REQUIRED`: "A `type: 'record_link'` screen field must declare `reference` — the name of the object the record belongs to (e.g. `reference: 'crm_account'`). Without it the field cannot name the record or link to its page." | | `defaultValue` | absent or `null` | `SCREEN_FIELD_RECORD_LINK_DEFAULT_VALUE_REQUIRED`: "A `type: 'record_link'` screen field must declare `defaultValue` — the id of the record it shows, usually a CEL value envelope over a flow variable (e.g. `defaultValue: { dialect: 'cel', source: 'account_id' }`). The field takes no input, so without it there is never a record to show." | | `required`, `options`, `placeholder`, `min`, `max` | present, with any value (`required: false` too) | `screenFieldRecordLinkRefusedKey(KEY)`: "A `type: 'record_link'` screen field shows a record and takes no input, so it cannot declare `KEY`: the run refuses any value submitted under its name. Remove the key, or use a `lookup` field to let the user pick a record." | | resume bag, field `NAME` | the key is present, any value | `invalid_value`: "Screen field "NAME" shows a record and takes no value — leave it out of the resume", inside the `INVALID_SCREEN_INPUT` envelope with the declared field list | New exports from `@objectstack/spec/automation` (api-surface and export-origins regenerated): `SCREEN_FIELD_RECORD_LINK_TYPE`, `SCREEN_FIELD_RECORD_LINK_REFERENCE_REQUIRED`, `SCREEN_FIELD_RECORD_LINK_DEFAULT_VALUE_REQUIRED`, `SCREEN_FIELD_RECORD_LINK_REFUSED_KEYS`, `screenFieldRecordLinkRefusedKey`. The `ScreenFieldSpec` wire contract only gains docblock text. No new `FieldErrorCode` member: `invalid_value` is the catalog's "rejected for a reason no other member names". ## Tests Measured on this branch. - `@objectstack/spec`: `screen-field-record-link.test.ts` (new, 9 tests) covers the accepted shapes (envelope id, literal id, help text, `visibleWhen`), each required key refused at its path, each refused key refused at its path (including `required: false`), several broken rules reported together, the refusal through `ScreenConfigSchema`, other types untouched (including `record`), and no tracker id in any refusal text. Full package: Test Files 805 passed (805), Tests 23263 passed, 1 todo. - `@objectstack/service-automation`: `screen-input-record-link-field.test.ts` (new, 6 tests) covers no value accepted, any value refused (`null`, empty, the shown id, another id, an object, `0`, `false`), refused while hidden, exact-member matching, and the headless verdict with a positive control. `builtin/screen-record-link-resume.test.ts` (new, 2 tests) runs the engine: two `create_record` nodes, then a result screen whose two links arrive typed `record_link` with `reference` and the created ids; a resume carrying a link's value is refused with nothing run after the screen; the clean resume completes. The account link is named `account`, after the very variable its id comes from, so a bound refused value would have broken the audit write after the screen. Full package: Test Files 209 passed (209), Tests 2707 passed (2707). - `@object-ui/app-shell`: `FlowRunner.recordLink-25872.test.tsx` (new, 6 tests) renders the real `FlowRunner` and the real `LookupField` over a fake data source whose objects name records by `company_name` / `full_name`, not `name`. It covers two links with the right names and hrefs; "—" and no link for an empty id; an unroutable object and a router-less mount showing the name unlinked; `visibleWhen`; Submit posting only the real input; and the seed/strip helpers. With the neighbours `views/__tests__/FlowRunner*`, `metadata-admin/previews/` and `spec-symbol-parity`: Test Files 160 passed (160), Tests 1794 passed (1794). The full app-shell suite is left to CI `Test UI`. - Dogfood: `flow-screen-record-link.dogfood.test.ts` (new) boots a real stack and drives the trigger and resume doors. The run creates an account and a contact and pauses on the result screen. Both fields arrive as `record_link` with their objects and the ids of the records this run wrote, and each id reads back through `/data/OBJECT/ID` with its `nameField` value (what the console shows). A resume carrying `account_link` or `contact_link: null` is refused `400 INVALID_SCREEN_INPUT` naming the field, and the clean resume completes the run. `pnpm --filter @objectstack/dogfood exec vitest run test/flow-screen-record-link.dogfood.test.ts`: Test Files 1 passed (1), Tests 2 passed (2). The full battery is not owed: no `!` in the title and no `(narrowing)` arm. - Type-check: `@objectstack/spec`, `@objectstack/service-automation` and `@objectstack/dogfood` `typecheck`, and `@object-ui/app-shell` `type-check`, exit 0 each. - Console: `pnpm exec turbo run build --filter=@object-ui/console` on the merge head `a2c646635b`, exit 0. ## Ablation Each mutation went through `scripts/ablation-replace.mjs`, which restored the file to its HEAD blob with `git diff HEAD` empty. - Spec: `if (field.type === SCREEN_FIELD_RECORD_LINK_TYPE) {` to `if (false as boolean) {` turned 5 of 9 tests red. - Engine: deleting the two guard lines of the `record_link` resume pass (so it skips every field) turned 3 tests red across both new files. - Engine: removing the `record_link` skip in `judgeHeadlessScreen` turned 1 test red. A first attempt used a replacement that was a substring of its anchor, and the tool refused it before any run; that attempt was void and was redone with a distinct replacement. - Console: `if (isRecordLinkScreenField(field)) {` to `if (false as boolean) {` in `ScreenFieldInput` turned 5 of 6 tests red. - Console: removing the skip in `initialScreenValues` turned 1 test red. The Submit test stayed green under that mutation because `screenResumeValues` still strips the key, which is the intended second layer. ## Gates Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `27c90bb39d` (115 commands; the same list as at the earlier `582dd2f9`). Each ran with its exit code captured before any pipe, and `--ran` reconciled them: "115 derived famil(ies) accounted for — 114 run, 1 NOT-MEASURED". The 114 that ran exited 0. They include `check-adr-0087-registration`, `check-changeset-no-major`, `check-empty-changeset`, `check:api-surface`, `check:authorable-surface`, `check:export-origins`, `check:docs`, `check:doc-authoring`, `check:cross-package-test-inputs`, `check:test-source-alias`, `check-sdui-manifest` (no record moves), `check:nul-bytes` and `check:liveness`. After merging `origin/main` (12 commits, none touching this diff's files) the spec was rebuilt and the ratchet families re-ran on the merge head `a2c646635b`: `check:generated` exit 0, and `check:type-check-debt` exit 0 ("0 raw tsc error(s) total, none above its recorded number"). `check:skill-examples` and the `check-plugin-teardown-shape` self-test first stopped on missing prerequisites (an unbuilt `client-react` and a shallow clone); both re-ran after the prerequisite was met and exited 0. `pnpm --dir ui run check:new-line-citations`: 0 new citations. Not measured locally, left to CI: `check:dual-build-cjs-loads`, which needs a full workspace `pnpm build` and stopped on that prerequisite with exit 3 before reading anything. This diff does not change any package's build shape. ## Acceptance notes - **Widening, as declared.** Before this PR `type` was an open string, so `type: 'record_link'` with any keys parsed and rendered a text input. It now refuses a `record_link` field without `reference` or `defaultValue`, or with an input key. No producer of a `record_link` screen field exists in this repository (`git grep` over `examples/`, `packages/` and `ui/` source finds none), so nothing that parses today is refused. The claim's `Clause-②: yes (widening)` is kept. - **The result screen still ends with Submit.** As the ruling's analysis said, a user who follows a link before submitting leaves the run paused, as closing the dialog does. Whether the dialog stays open across that navigation depends on the host page; this PR does not change it. - **A record that cannot be read** (deleted, or denied to the user) settles `LookupField`'s readonly branch on "—", and the link around it still points at the record page, which then answers for itself. The name is never replaced with the raw id. - **Case.** The console reads `record_link` exactly, as the spec and engine do, unlike `file` and `number`, which the console lowercases. A field declared `type: 'Record_Link'` is an ordinary text input on every layer. - **Skill and eval are a follow-up.** The skills surface is governed, so the owed skill text and eval prompt (#26184) are in #26347 (`skills/objectstack-automation`, an entry in `evals/flows-triggers-approvals.json`). This PR does not touch `skills/**`. - After a published `@objectstack/*` release carries this, the hotcrm seat can lift hotcrm#2285's `Blocked-by:`. --- _Generated by [Claude Code](https://claude.ai/code/session_0115tbh4XfgivQD2RpHHwdMz)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_0115tbh4XfgivQD2RpHHwdMz --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c19ad22 commit 4548ba3

16 files changed

Lines changed: 1094 additions & 18 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-automation': minor
4+
'@object-ui/app-shell': minor
5+
---
6+
7+
feat(spec,automation,app-shell): a read-only `record_link` screen field shows a record the run created, by its display name, as a link to its record page
8+
9+
Clause-②: yes (widening)
10+
Metadata-protocol: added
11+
12+
A screen flow can now end on a result screen that links the records it created, instead of a toast that points at nothing.
13+
14+
```ts
15+
{ id: 'done', type: 'screen', config: { title: 'Lead converted', fields: [
16+
{ name: 'account_link', label: 'Account', type: 'record_link',
17+
reference: 'crm_account', defaultValue: { dialect: 'cel', source: 'account.id' } },
18+
] } },
19+
```
20+
21+
- **`@objectstack/spec`**: `ScreenFieldConfigSchema` gains the `type` member `'record_link'` (`SCREEN_FIELD_RECORD_LINK_TYPE`). On it, `reference` (the record's object) and `defaultValue` (a value slot that evaluates to the record's id) are required, and `required`, `options`, `placeholder`, `min` and `max` are refused, each at its own key with the exported refusal text (`SCREEN_FIELD_RECORD_LINK_REFERENCE_REQUIRED`, `SCREEN_FIELD_RECORD_LINK_DEFAULT_VALUE_REQUIRED`, `screenFieldRecordLinkRefusedKey`). The name is `record_link`, not `record`, because `record` is already an object field type (a name-keyed map of sub-objects) and a screen field's `type` uses the same vocabulary. Every other `type` value parses as before.
22+
- **`@objectstack/service-automation`**: the resume refuses any value sent under a `record_link` field's name, `null` included and visible or not, with `400 INVALID_SCREEN_INPUT` and the field code `invalid_value` ("shows a record and takes no value"). The value is never bound to a flow variable, the refusal keeps the pause, and a resume that leaves the field out still lands. A caller's param that shares the field's name never counts as answering the screen, so a result screen always pauses.
23+
- **`@object-ui/app-shell`**: the flow screen dialog shows a `record_link` field as the record's display name (the object's `nameField`, through the lookup widget's read-only branch) inside a link to `/apps/APP/OBJECT/record/ID`. An empty id shows "—" with no link, and an object the console cannot route to shows the name without a link. `visibleWhen` works as on any field. The dialog never seeds or posts a value for the field.

‎content/docs/automation/flows.mdx‎

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,40 @@ wizard can collect everything first and write once, after its last screen:
818818
The target `file` field's own `accept` / `maxSize` still apply at the write
819819
that claims the file.
820820

821+
#### A read-only record link
822+
823+
`type: 'record_link'` shows a record the run already holds the id of, so a
824+
screen flow can end on a result screen that links the records it created:
825+
826+
```typescript
827+
{ id: 'create_account', type: 'create_record', config: { objectName: 'crm_account',
828+
fields: { name: { dialect: 'cel', source: 'company' } }, outputVariable: 'account' } },
829+
{ id: 'create_contact', type: 'create_record', config: { objectName: 'crm_contact',
830+
fields: { last_name: { dialect: 'cel', source: 'last_name' } }, outputVariable: 'contact' } },
831+
{ id: 'done', type: 'screen', config: { title: 'Lead converted', fields: [
832+
{ name: 'account_link', label: 'Account', type: 'record_link',
833+
reference: 'crm_account', defaultValue: { dialect: 'cel', source: 'account.id' } },
834+
{ name: 'contact_link', label: 'Contact', type: 'record_link',
835+
reference: 'crm_contact', defaultValue: { dialect: 'cel', source: 'contact.id' } },
836+
] } },
837+
```
838+
839+
- **The console shows the record's name as a link.** The flow dialog fetches the
840+
record and shows its display name (the object's `nameField`), as a link to the
841+
record's page. An empty id shows "—". `visibleWhen` works as on any field.
842+
- **`reference` and `defaultValue` are required.** `reference` names the
843+
record's object. `defaultValue` is a value slot that evaluates to the record's
844+
id when the screen is served, usually a CEL envelope over a flow variable.
845+
- **It takes no input.** `required`, `options`, `placeholder`, `min` and `max`
846+
are refused on it at publish. The console posts nothing for it, and a resume
847+
that carries any value under its name is refused with `invalid_value` inside
848+
the run's `INVALID_SCREEN_INPUT`. The value is never bound to a flow variable.
849+
- **The screen still ends with Submit.** A result screen is an ordinary screen:
850+
the run completes when the user submits it. A user who follows a link first
851+
leaves the run paused, as closing the dialog does.
852+
- **Why not `record`?** That name is already an object field type (a name-keyed
853+
map of sub-objects), and a screen field's `type` uses the same vocabulary.
854+
821855
**Screen (object form):**
822856

823857
Set `config.objectName`
@@ -1255,11 +1289,14 @@ POST /api/v1/automation/{flow}/runs/{runId}/resume
12551289
— declared fields: 'kind', 'escalation_reason'
12561290
```
12571291

1258-
Two conditions are refused, both reported at once and both with
1259-
`code: 'INVALID_SCREEN_INPUT'`:
1292+
These conditions are refused, all reported at once and all with
1293+
`code: 'INVALID_SCREEN_INPUT'` (the value shapes of `type: 'number'` and
1294+
`type: 'file'`, and the `min` / `max` bounds, are described with those fields
1295+
above):
12601296

12611297
- a **`required` field the caller was actually asked for** is missing (an empty
12621298
or blank string counts as missing);
1299+
- a value was sent under a **`type: 'record_link'` field**, which takes none;
12631300
- a key the screen **never declared** was sent.
12641301

12651302
`visibleWhen` is evaluated against the **submitted values** first, so a hidden

‎content/docs/references/automation/builtin-node-config.mdx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ A value: a CEL value envelope `{ dialect: 'cel', source }` evaluated by the expr
278278
| :--- | :--- | :--- | :--- |
279279
| **name** | `string` | ✅ | Field name (the flow variable the value binds to) — a name without a leading `$` (the `$` names are the flow engine's own), read as `{{ name }}` |
280280
| **label** | `string` | optional | Display label |
281-
| **type** | `string` | optional | Input type — an open widget hint (text, number, select, …); `file` renders the platform upload widget and the screen resumes with the uploaded file's id, which a later node writes into a `file` field |
281+
| **type** | `string` | optional | Input type — an open widget hint (text, number, select, …); `file` renders the platform upload widget and the screen resumes with the uploaded file's id, which a later node writes into a `file` field; `record_link` shows the `reference` record whose id `defaultValue` evaluates to, by its display name, as a link to its record page, and takes no input |
282282
| **required** | `boolean` | optional | Whether a value is required to submit |
283283
| **options** | `{ value: any; label: string }[]` | optional | Choices for a select-style field; option values must stay distinct when read as text |
284284
| **defaultValue** | `any` | optional | Prefilled value: a CEL value envelope `{ dialect: 'cel', source }` evaluated when the screen is served (the client receives the value, never the envelope), or a literal written as it is — a `{…}` template token is refused |
@@ -287,7 +287,7 @@ A value: a CEL value envelope `{ dialect: 'cel', source }` evaluated by the expr
287287
| **min** | `number` | optional | Minimum accepted value (numeric fields); enforced on resume |
288288
| **max** | `number` | optional | Maximum accepted value (numeric fields); enforced on resume |
289289
| **inlineHelpText** | `string` | optional | Help text displayed below the field |
290-
| **reference** | `string` | optional | Target object name (snake_case) whose records a `type: 'lookup'` field picks from; REQUIRED when `type` is `lookup` |
290+
| **reference** | `string` | optional | Target object name (snake_case) whose records a `type: 'lookup'` field picks from, or that a `type: 'record_link'` field's record belongs to; REQUIRED when `type` is `lookup` or `record_link` |
291291

292292

293293
---
@@ -300,7 +300,7 @@ A value: a CEL value envelope `{ dialect: 'cel', source }` evaluated by the expr
300300
| :--- | :--- | :--- | :--- |
301301
| **name** | `string` | ✅ | Field name (the flow variable the value binds to) — a name without a leading `$` (the `$` names are the flow engine's own), read as `{{ name }}` |
302302
| **label** | `string` | optional | Display label |
303-
| **type** | `string` | optional | Input type — an open widget hint (text, number, select, …); `file` renders the platform upload widget and the screen resumes with the uploaded file's id, which a later node writes into a `file` field |
303+
| **type** | `string` | optional | Input type — an open widget hint (text, number, select, …); `file` renders the platform upload widget and the screen resumes with the uploaded file's id, which a later node writes into a `file` field; `record_link` shows the `reference` record whose id `defaultValue` evaluates to, by its display name, as a link to its record page, and takes no input |
304304
| **required** | `boolean` | optional | Whether a value is required to submit |
305305
| **options** | `{ value: any; label: string }[]` | optional | Choices for a select-style field; option values must stay distinct when read as text |
306306
| **defaultValue** | `any` | optional | Prefilled value: a CEL value envelope `{ dialect: 'cel', source }` evaluated when the screen is served (the client receives the value, never the envelope), or a literal written as it is — a `{…}` template token is refused |
@@ -309,7 +309,7 @@ A value: a CEL value envelope `{ dialect: 'cel', source }` evaluated by the expr
309309
| **min** | `number` | optional | Minimum accepted value (numeric fields); enforced on resume |
310310
| **max** | `number` | optional | Maximum accepted value (numeric fields); enforced on resume |
311311
| **inlineHelpText** | `string` | optional | Help text displayed below the field |
312-
| **reference** | `string` | optional | Target object name (snake_case) whose records a `type: 'lookup'` field picks from; REQUIRED when `type` is `lookup` |
312+
| **reference** | `string` | optional | Target object name (snake_case) whose records a `type: 'lookup'` field picks from, or that a `type: 'record_link'` field's record belongs to; REQUIRED when `type` is `lookup` or `record_link` |
313313

314314
### Nested Shape: `ScreenFieldConfig.options[number]`
315315

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// Flow screen record-link fixture (#25872, contract half #26304).
4+
//
5+
// The shape the ruling asks for: a screen flow that creates records ends on a
6+
// result screen that links them, instead of a toast that points at nothing.
7+
// The flow asks for a company and a contact, writes an account and a contact,
8+
// and pauses on a last screen of two read-only `type: 'record_link'` fields
9+
// whose `defaultValue` envelopes read the created records' ids.
10+
//
11+
// Both objects name their records by a field that is NOT `name`
12+
// (`company_name`, `full_name`, ADR-0079 `nameField`), so a client that showed
13+
// a `name` column or the raw id would show the wrong thing.
14+
15+
import { defineStack } from '@objectstack/spec';
16+
import { ObjectSchema, Field } from '@objectstack/spec/data';
17+
import type { Flow } from '@objectstack/spec/automation';
18+
19+
export const SrlAccount = ObjectSchema.create({
20+
name: 'srl_account',
21+
// [ADR-0090 D1] grandfather stamp: the gate under test is the result screen,
22+
// not owner-sharing.
23+
sharingModel: 'public_read_write',
24+
label: 'Account',
25+
pluralLabel: 'Accounts',
26+
nameField: 'company_name',
27+
fields: {
28+
company_name: Field.text({ label: 'Company', required: true }),
29+
},
30+
});
31+
32+
export const SrlContact = ObjectSchema.create({
33+
name: 'srl_contact',
34+
sharingModel: 'public_read_write',
35+
label: 'Contact',
36+
pluralLabel: 'Contacts',
37+
nameField: 'full_name',
38+
fields: {
39+
full_name: Field.text({ label: 'Full name', required: true }),
40+
account: Field.lookup('srl_account', { label: 'Account' }),
41+
},
42+
});
43+
44+
const CEL = (source: string) => ({ dialect: 'cel' as const, source });
45+
46+
/**
47+
* `srl_convert` — start → ask (company, contact) → create account → create
48+
* contact → result (two record links) → end.
49+
*/
50+
export const flowScreenRecordLink: Flow = {
51+
name: 'srl_convert',
52+
label: 'Convert with a result screen',
53+
type: 'screen',
54+
status: 'active',
55+
runAs: 'user',
56+
nodes: [
57+
{ id: 'start', type: 'start', label: 'Start' },
58+
{
59+
id: 'ask',
60+
type: 'screen',
61+
label: 'Convert',
62+
config: {
63+
title: 'Convert',
64+
fields: [
65+
{ name: 'company', label: 'Company', type: 'text', required: true },
66+
{ name: 'contact_name', label: 'Contact', type: 'text', required: true },
67+
],
68+
},
69+
},
70+
{
71+
id: 'create_account',
72+
type: 'create_record',
73+
label: 'Create account',
74+
config: { objectName: 'srl_account', fields: { company_name: CEL('company') }, outputVariable: 'account' },
75+
},
76+
{
77+
id: 'create_contact',
78+
type: 'create_record',
79+
label: 'Create contact',
80+
config: {
81+
objectName: 'srl_contact',
82+
fields: { full_name: CEL('contact_name'), account: CEL('account.id') },
83+
outputVariable: 'contact',
84+
},
85+
},
86+
{
87+
id: 'result',
88+
type: 'screen',
89+
label: 'Created',
90+
config: {
91+
title: 'Created',
92+
fields: [
93+
{ name: 'account_link', label: 'Account', type: 'record_link', reference: 'srl_account', defaultValue: CEL('account.id') },
94+
{ name: 'contact_link', label: 'Contact', type: 'record_link', reference: 'srl_contact', defaultValue: CEL('contact.id') },
95+
],
96+
},
97+
},
98+
{ id: 'end', type: 'end', label: 'End' },
99+
],
100+
edges: [
101+
{ id: 'e1', source: 'start', target: 'ask' },
102+
{ id: 'e2', source: 'ask', target: 'create_account' },
103+
{ id: 'e3', source: 'create_account', target: 'create_contact' },
104+
{ id: 'e4', source: 'create_contact', target: 'result' },
105+
{ id: 'e5', source: 'result', target: 'end' },
106+
],
107+
};
108+
109+
export const screenRecordLinkStack = defineStack({
110+
manifest: {
111+
id: 'com.dogfood.flow-screen-record-link',
112+
namespace: 'srl',
113+
version: '0.0.0',
114+
type: 'app',
115+
name: 'Flow Screen Record Link Fixture',
116+
description: 'A screen flow that creates two records and ends on a screen that links them.',
117+
},
118+
objects: [SrlAccount, SrlContact],
119+
flows: [flowScreenRecordLink],
120+
});

0 commit comments

Comments
 (0)