Builtin Node Config
Builtin Node Config protocol schemas
@module automation/builtin-node-config
Config contracts for the remaining flat builtins — the CRUD quartet
(get_record / create_record / update_record / delete_record),
screen, and map (#4045). Sibling of io-node-config.zod.ts
(notify / http) and control-flow.zod.ts (loop / parallel / try_catch).
Provenance — written from the executors, not from the forms
Each schema was derived by reading what the executor actually does with
node.config (service-automation/builtin/crud-nodes.ts,
screen-nodes.ts, map-node.ts), not by transcribing the hand-written
configSchema literal on the node's descriptor. The two artifacts are
reconciled bidirectionally by builtin-node-form-zod-ledger.test.ts in
service-automation; a Zod copied from the form would make that
reconciliation a tautology (#4045).
Writing these against the executors is what surfaced the drift the
reconciliation exists to catch — keys the executors read that no form
offered anywhere (get_record.fields, screen.recordId, the screen
field-item keys options/defaultValue/placeholder, map.indexVariable,
map.input), and the undeclared map.flow alias, which graduated into the
ADR-0087 D2 conversion layer like notify.source before it.
What these schemas are wired to (#4277)
Live execute-time contracts: each executor parse()s its config against
its schema before running (service-automation's parse-config.ts), so
type and required violations refuse the node as a guard. All of these
parse the RAW stored config — their typed slots are strings (or unknown
where values interpolate), so {token} templates pass and resolve at the
executor's existing interpolation points.
Unknown keys — closed here too, as of #4001 批 9
These contracts used to say "unknown keys are rejected earlier, at
registerFlow() (the tightened #4059 check); the parse here strips them."
The registration walk is still the first and more informative door — it
descends NESTED config against the descriptor's JSON Schema, which is how it
catches fields[0].visibleIf (#3528) and not just top-level typos — but
"some other door is closed" is the exact reasoning #4001 exists to retire:
the sibling of every guard in this campaign turned out to leave the other
doors open, because its author was fixing one bug rather than auditing a
surface. A config reaching parse() without passing registration (tooling
that parses a contract directly, a host composing the engine itself) is no
longer silently trimmed.
The two doors are kept in agreement by builtin-node-form-zod-ledger.test.ts,
which reconciles these key sets against the descriptors' in both directions.
The per-key prescriptions below are the same curation the registration
rejection carries in FLOW_NODE_UNKNOWN_KEY_GUIDANCE — the campaign's
finding is that a bespoke guard's detection generalizes for free the moment
a default flips, while its PROSE does not, so the prose is copied to the new
door rather than left behind at the old one.
Deliberately absent:
assignment— its config cannot be described by a fixed key set: with noassignmentswrapper the TOP-LEVEL config keys ARE the author's variable names (logic-nodes.ts). The ledger test pins that exemption with its reason instead of pretending a shape.decision/script/subflow/wait/connector_action— the descriptor-schemaless class (config-schemas.test.ts).waitandconnector_actionkeep their contracts in FlowNodeSchema's sibling blocks (waitEventConfig/connectorConfig); the other three publish executor-derived config contracts inschemaless-node-config.zod.ts(#4278) — separate from this module because they must NOT grow into descriptorconfigSchemas (the forms they describe stay hand-written in objectui, reconciled by a test there).
Source: packages/spec/src/automation/builtin-node-config.zod.ts
TypeScript Usage
import { CreateRecordConfigSchema, DeleteRecordConfigSchema, GetRecordConfigSchema, MapConfigSchema, ScreenConfigSchema, ScreenFieldConfigSchema, UpdateRecordConfigSchema } from '@objectstack/spec/automation';
import type { CreateRecordConfig, DeleteRecordConfig, GetRecordConfig, MapConfig, ScreenConfig, ScreenFieldConfig, UpdateRecordConfig } from '@objectstack/spec/automation';
// Validate data
const result = CreateRecordConfigSchema.parse(data);CreateRecordConfig
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| objectName | string | ✅ | Object to insert into |
| fields | Record<string, any> | optional | Field values to write on the new record |
| outputVariable | string | optional | Flow variable bound to the created record |
DeleteRecordConfig
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| objectName | string | ✅ | Object to delete from |
| filter | Record<string, any> | optional | Field/value pairs identifying the record(s) to delete |
| multi | boolean | optional | Declare bulk intent: delete every row the filter matches (default false — a predicate delete without it is refused by the engine) |
GetRecordConfig
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| objectName | string | ✅ | Object to query |
| filter | Record<string, any> | optional | Field/value pairs to match (operator values like {"$ne": null} are preserved) |
| fields | string[] | optional | Field projection — only these fields are read (default: all) |
| limit | number | optional | Max records to return; >1 switches to a multi-record query |
| outputVariable | string | optional | Flow variable the result is bound to |
MapConfig
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| collection | string | any[] | ✅ | Template/variable resolving to the array to process (an inline array is accepted) |
| flowName | string | ✅ | Subflow run for each item — it may pause (e.g. an approval) |
| iteratorVariable | string | optional (default: "item") | Variable holding the current item |
| indexVariable | string | optional | Optional variable holding the current index |
| itemObject | string | optional | When items are records, the object they belong to (exposes each item as the child's record) |
| input | Record<string, any> | optional | Params passed to each item's subflow (interpolated per item) |
| outputVariable | string | optional | Each item's subflow output, collected in order |
ScreenConfig
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| title | string | optional | Heading shown above the screen |
| description | string | optional | Body text shown under the heading |
| fields | { name: string; label?: string; type?: string; required?: boolean; … }[] | optional | Input fields collected on this screen |
| waitForInput | boolean | optional | Pause to show the screen even with no fields; false forces a server pass-through |
| objectName | string | optional | Render this object's full create/edit form instead of a flat field list |
| idVariable | string | optional | Object form only: variable bound to the saved record's id |
| mode | Enum<'create' | 'edit'> | optional | Object form only: create or edit |
| recordId | string | optional | Object form only: id of the record to edit (required for mode: 'edit' to be useful) |
| defaults | Record<string, any> | optional | Object form only: prefilled values |
ScreenFieldConfig
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | ✅ | Field name (the flow variable the value binds to) |
| label | string | optional | Display label |
| type | string | optional | Input type |
| required | boolean | optional | Whether a value is required to submit |
| options | { value: any; label: string }[] | optional | Choices for a select-style field |
| defaultValue | any | optional | Prefilled value (interpolates {token} templates) |
| placeholder | string | optional | Input placeholder text |
| visibleWhen | string | optional | CEL predicate controlling visibility, evaluated client-side |
UpdateRecordConfig
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| objectName | string | ✅ | Object to update |
| filter | Record<string, any> | optional | Field/value pairs identifying the record(s) to update |
| fields | Record<string, any> | optional | Field values to write |
| multi | boolean | optional | Declare bulk intent: update every row the filter matches (default false — a predicate update without it is refused by the engine) |