ObjectStackObjectStack

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 no assignments wrapper 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). wait and connector_action keep their contracts in FlowNodeSchema's sibling blocks (waitEventConfig / connectorConfig); the other three publish executor-derived config contracts in schemaless-node-config.zod.ts (#4278) — separate from this module because they must NOT grow into descriptor configSchemas (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

PropertyTypeRequiredDescription
objectNamestringObject to insert into
fieldsRecord<string, any>optionalField values to write on the new record
outputVariablestringoptionalFlow variable bound to the created record

DeleteRecordConfig

Properties

PropertyTypeRequiredDescription
objectNamestringObject to delete from
filterRecord<string, any>optionalField/value pairs identifying the record(s) to delete
multibooleanoptionalDeclare bulk intent: delete every row the filter matches (default false — a predicate delete without it is refused by the engine)

GetRecordConfig

Properties

PropertyTypeRequiredDescription
objectNamestringObject to query
filterRecord<string, any>optionalField/value pairs to match (operator values like {"$ne": null} are preserved)
fieldsstring[]optionalField projection — only these fields are read (default: all)
limitnumberoptionalMax records to return; >1 switches to a multi-record query
outputVariablestringoptionalFlow variable the result is bound to

MapConfig

Properties

PropertyTypeRequiredDescription
collectionstring | any[]Template/variable resolving to the array to process (an inline array is accepted)
flowNamestringSubflow run for each item — it may pause (e.g. an approval)
iteratorVariablestringoptional (default: "item")Variable holding the current item
indexVariablestringoptionalOptional variable holding the current index
itemObjectstringoptionalWhen items are records, the object they belong to (exposes each item as the child's record)
inputRecord<string, any>optionalParams passed to each item's subflow (interpolated per item)
outputVariablestringoptionalEach item's subflow output, collected in order

ScreenConfig

Properties

PropertyTypeRequiredDescription
titlestringoptionalHeading shown above the screen
descriptionstringoptionalBody text shown under the heading
fields{ name: string; label?: string; type?: string; required?: boolean; … }[]optionalInput fields collected on this screen
waitForInputbooleanoptionalPause to show the screen even with no fields; false forces a server pass-through
objectNamestringoptionalRender this object's full create/edit form instead of a flat field list
idVariablestringoptionalObject form only: variable bound to the saved record's id
modeEnum<'create' | 'edit'>optionalObject form only: create or edit
recordIdstringoptionalObject form only: id of the record to edit (required for mode: 'edit' to be useful)
defaultsRecord<string, any>optionalObject form only: prefilled values

ScreenFieldConfig

Properties

PropertyTypeRequiredDescription
namestringField name (the flow variable the value binds to)
labelstringoptionalDisplay label
typestringoptionalInput type
requiredbooleanoptionalWhether a value is required to submit
options{ value: any; label: string }[]optionalChoices for a select-style field
defaultValueanyoptionalPrefilled value (interpolates {token} templates)
placeholderstringoptionalInput placeholder text
visibleWhenstringoptionalCEL predicate controlling visibility, evaluated client-side

UpdateRecordConfig

Properties

PropertyTypeRequiredDescription
objectNamestringObject to update
filterRecord<string, any>optionalField/value pairs identifying the record(s) to update
fieldsRecord<string, any>optionalField values to write
multibooleanoptionalDeclare bulk intent: update every row the filter matches (default false — a predicate update without it is refused by the engine)

On this page