Repository navigation
fix(workflow-executor): coerce the pre-recorded update value, invalid one is a configuration error - #2008
Conversation
8 new issues
|
|
Coverage Impact ⬆️ Merging this pull request will increase total coverage on Modified Files with Diff Coverage (2)
🛟 Help
|
… one is a configuration error A build-time fieldName + value on an Update Data step was written as is, while the AI and user-override paths coerce it to the field's type. It now goes through coerceFieldValue too; a value the field's type rejects raises InvalidPreRecordedValueError (errorKind: configuration) naming the field and the reason, and nothing is written. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…umber when coercing an update value A Date value with a timezone offset (+02:00) was rejected while it names an exact instant; it is now accepted, a datetime without any zone still fails. A blank string on a Number field was coerced to 0 and written; it now fails like any other non-numeric string. Both apply to the pre-recorded and override paths; the AI tool schema keeps plain z.coerce.number(). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
3679c17 to
4b96ea5
Compare
…ue on a relationship field z.coerce.number() still read false and [] as 0 and true as 1, and a relationship field skipped coercion, so both were written raw. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…I path's coercion rules The stricter Date and Number rules now apply only to a value a person wrote (pre-recorded or overridden), so the AI tool schemas and AI-returned values match the front again. The pre-recorded value error no longer surfaces zod's wording to the end user. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nd keep number and json values exact A relationship can't be updated through the agent's attribute route, null included, so a pinned relationship field fails as a configuration error with or without a value. A decimal string a double would round is written as is, a Json object or array is accepted, and non-decimal strings are rejected on the person-written paths. A pending value accepted without override is coerced leniently so rows saved by an older executor still go through, and a pending field removed from the schema fails instead of being written raw. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… json scalars The agent takes a string on a Number column only as an integer, so only an integer too large for a double stays a string. A Json column accepts any value, so booleans and numbers pass too. Also covers a composite field and drops an unreachable fallback. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ip override as configuration Both come from the schema or the step, which only an Admin can change, so they keep the front's contact-an-Admin banner instead of the generic unexpected-error message. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…is rejected The strict path logged a bare "Invalid input" and the lenient one leaked the union's regex. Both now name the accepted shapes; the user message is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…view's accept is coerced invokeWithTool returns the tool args unparsed, so coerceFieldValue is the runtime check on AI values: pin an offset Date, Json scalars and objects, and a bigint string at executor level. The run view accepts by sending the pending value back, which takes the strict path. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…an unsafe fraction On the strict path a decimal string with only zeros after the point becomes its integer string, which the agent's BigInt check accepts, and a real fraction beyond Number.MAX_SAFE_INTEGER is rejected instead of being silently rounded. The lenient and AI paths are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… the safe range Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PMerlet
left a comment
There was a problem hiding this comment.
Spec (PRD-628): conforms. A pinned value is coerced before the Automatic write, and an invalid one fails the step without writing.
…s and AI-chosen relationships
- A Point takes [lon, lat] or "lon,lat" and is written as the "lon,lat"
string, the only shape the agent's TypeGetter accepts. A pinned
"lon,lat" value was rejected since the coercion landed.
- A strict decimal with a real fraction is rejected whenever a double
cannot hold every significant digit, not only beyond
MAX_SAFE_INTEGER ("123456789.123456789", "4503599627370496.5").
- A relationship named by the AI raises FieldNotFoundError, which tells
the operator to rephrase the prompt, instead of the configuration
error meant for a pinned field.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PMerlet
left a comment
There was a problem hiding this comment.
Approved after /verify-fixes run 2008-20261030082124-PMerlet-verify: 3 findings of 2008-20261029153303-PMerlet closed or accepted, none reopened.
## @forestadmin/workflow-executor [1.35.4](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/workflow-executor@1.35.3...@forestadmin/workflow-executor@1.35.4) (2026-10-30) ### Bug Fixes * **workflow-executor:** coerce the pre-recorded update value, invalid one is a configuration error ([#2008](#2008)) ([996d623](996d623))

Part of PRD-628 (batch 1/4: executor)
Problem
On an Update Data step pinned with
fieldName+value, the build-time value was written as is. The AI paths and the user override coerce the value to the field's type throughcoerceFieldValue, so an invalid enum or a malformed date could reach the datasource unchecked on the automatic path only.Change
handleFirstCallresolves the pinned field first, then runs the pre-recorded value throughcoerceFieldValuebefore the FullyAutomated / confirmation split.InvalidPreRecordedValueError(WorkflowConfigurationError→errorKind: 'configuration'). Nothing is written. The technical message (logs) carries the zod reason; the user message names the field only:The value set on this step can't be written to the "Status" field. Edit the step to set a valid value.RelationshipFieldNotUpdatableError(configuration), pinned with or without a value (nullincluded) or overridden at accept.updateRecordsends attributes only, and the agent'sRecordValidatorrejects a relation there ("The record data is empty" / "Unexpected schema type"), so nothing can be unlinked this way. User message:The "Orders" field is a relationship, which this step can't update. Edit the step to pick another field.A configuration kind keeps the front's contact-an-Admin banner and offers no rewind (forestadmin ADR 2026-09-23), which fits: only an Admin can change the step.PendingFieldNotFoundError(configuration, schema drift likeFieldTypeMissingError) instead of a raw write.expected a number or a decimal string, e.g. 12, -3 or 12.5on both the strict and lenient paths (it used to log a bareInvalid input, or leak the union's regex). Technical message only; the user message is unchanged.coerceFieldValuetakes options{ strict, toError }and now requires a resolved field; the AI path throwsFieldNotFoundErrorbefore coercing, also when the AI names a relationship (the operator is told to rephrase the prompt, not to edit a step that pins nothing).Coercion rules
strict: pre-recorded, override)Zonly, as onmainZor offsetZor offset. No zone → rejectedz.coerce.number(), as onmainmain(""→ 0,true→ 1)"12","-3","12.50"). Blank, booleans, arrays,0x10,1e3,Infinity→ rejected. A zero fraction is dropped ("9007199254740993.0"→"9007199254740993", kept exact); a real fraction a double cannot hold digit for digit ("123456789.123456789","4503599627370496.5","12345678901234567890.5") → rejected instead of roundedmain[lon, lat], as onmain[lon, lat]or"lon,lat", written as"lon,lat"[lon, lat]or"lon,lat", written as"lon,lat"An integer string beyond
Number.MAX_SAFE_INTEGER(a bigint column, e.g."12345678901234567890") is written as the string, on every coercion path: the agent accepts a string on a Number column only whenBigInt()parses it. Every other decimal string is converted to a number ("0.0000001"→1e-7). A Point is written as the"lon,lat"string because the agent'sTypeGettertakes no other shape: onmainthe array from the AI or an override was rejected by the agent, and only a pinned"lon,lat"got through. Contract for the front batches: a Date value must carryZor an offset.A pending value accepted without override is coerced leniently. The current front never takes that path: the run view's accept (
submitFieldUpdate) always sends{ value, userConfirmed: true }, with the staged value falling back to the suggestion, so every accept from the front is coerced strictly. The lenient path only serves a client that accepts without sending a value.Behavior notes for reviewers
pendingData. An invalid value fails at the first call as a configuration error, where it used to fail at accept time with a genericStepStateError.FieldTypeMissingError(configuration), as the override path already does.""or a non-decimal string on a Number: now fails withStepStateErrorinstead of being written. On a relationship field: fails withRelationshipFieldNotUpdatableError(configuration).forestadminmain,toExecutorValuesends a Number as a number (blank →0before sending), a Date astoISOString()or the suggestion as stored, a Dateonly asYYYY-MM-DDand Json as a string. Precision on a bigint column is still lost on accept, because the front'stoNumberrounds the pending string;mainrounded it too.invokeWithToolreturns the raw tool args), socoerceFieldValueis the runtime check. AI values now go through the non-AI schemas: a Date with an offset and a non-string Json value are written, and a bigint string is kept exact.""→ 0 andtrue→ 1 on a Number are unchanged.1.33.2is already published without this change, so the floor is the version tagged after merge.forestadminmain) has no value input yet and the server forwardsvalueverbatim, so today only a hand-edited BPMN reaches this path.Tests
update-record-step-executor.test.ts:pendingData), datetime withZ, with an offset, without a zone, impossible date, bigint strings kept exact ("12345678901234567890", negative too),"0.0000001"→1e-7,"12.50","007","-3", a zero fraction dropped ("9007199254740993.0"and"-12345678901234567890.000"kept as exact integer strings,"12.0"→12), a fraction a double cannot hold rejected ("12345678901234567890.5","4503599627370496.5","123456789.123456789","0.1000000000000000000001","1.00000000000000000001"), Point"2.35,48.85"and[2.35, 48.85]written as"2.35,48.85", malformed Points rejected, blank / boolean / array / non-decimal strings rejected, the expected number format named in the logged message only, Json object, array, string, boolean and number written as is, object on a composite field written as is, invalid JSON string rejected, relationship field rejected for"42",nulland no value, type-less field →FieldTypeMissingError,0,false/"false",null.nullincluded) rejected as configuration, removed field →PendingFieldNotFoundError(configuration, logged message checked). The run view's real accept (pending value sent back asvalue) is coerced strictly: a pending""is rejected, a pending bigint string stays exact. A client accepting without sending a value gets lenient coercion: pending blank Number →0, offset Date, Json object and bigint string written as is.invokeWithToolreturns unparsed args): an offset Date, a Json object / boolean / number and a bigint string are written as is;""/trueon a Number still written0/1; a non-number logs the expected format; a Point array is written as"lon,lat"; a relationship named by the AI fails withFieldNotFoundError(not configuration), nothing written. A separate test checks only the tool schema sent to the model: it acceptsZand rejects an offset oninput.value.Red-check: the two diagnostic tests fail against the previous number coercion, the four safe-range tests fail without the zero-fraction / beyond-range rule, and the 12 tests added for the lossy-decimal, Point and AI-relationship fixes fail without them.
Package tests (2686 passed, 8 skipped), lint (0 errors) and
tscare green; every line this PR adds toupdate-record-step-executor.tsanderrors.tsis covered.🤖 Generated with Claude Code
Note
Add strict coercion and configuration errors for update-record pre-recorded values
InvalidPreRecordedValueError,RelationshipFieldNotUpdatableError, andPendingFieldNotFoundErrorin errors.tsChanges since #2008 opened
lon,latstring in the update-record executor [ecfbbe8]UpdateRecordStepExecutorto throwFieldNotFoundErrorwhen the AI selects a relationship field [ecfbbe8]update-record-step-executortests for numeric coercion, Point coercion, and relationship-field handling [ecfbbe8]workflow-executorCLAUDE.md[ecfbbe8]Macroscope summarized dc63ca0.