fix(workflow-executor): tolerate orchestrator collection-schema drift#1613
Conversation
The orchestrator deploys independently and evolves the collection schema it returns (adds fields like relatedPrimaryKey/referenceField, may omit step-specific props). With .strict(), any additive change made the whole step fail at parse — even for fields the step never uses. Strip unknown keys on the orchestrator collection schema (CollectionSchema + nested FieldSchema/ActionSchema) and keep only structural fields required. Demote step-specific props that have a use-time guard: actions -> optional (default []), guarded by NoActionsError in trigger-action; field type -> optional, guarded by buildZodSchemaForField's string fallback in update-record. We now fail only when a step genuinely lacks what it needs, at execution. Produced/envelope schemas and frontend HTTP bodies keep .strict(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Coverage Impact Unable to calculate total coverage change because base branch coverage was not found. Modified Files with Diff Coverage (3)
🛟 Help
|
…umn type Review follow-up. Making FieldSchema.type optional (resilience) introduced a silent mis-coercion: a non-relationship field whose type the orchestrator omits fell through buildZodSchemaForField to z.string(), and coerceFieldValue skipped coercion (type == null) — so in fully-automated mode a wrong-typed value could be written to the record with no error. update-record now throws FieldTypeMissingError use-time when a writable field has no type (the step genuinely lacks what it needs), instead of the string fallback. Relationship fields (type intentionally null) still pass through. The collection schema stays tolerant (type optional) so other step types remain drift-resilient. Also: scope the strip-policy comment to the whole collection schema section and trim the verbose CLAUDE.md boundary-validation bullet (review nits). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 new issue
|
| z.object({ | ||
| fieldName: z.literal(f.displayName), | ||
| value: buildZodSchemaForField(f).nullable(), | ||
| value: buildZodSchemaForField(f, schema.collectionName).nullable(), |
There was a problem hiding this comment.
AI path fails in bulk — at odds with this PR's own goal
This maps over all nonRelationFields. Since buildZodSchemaForField now throws FieldTypeMissingError as soon as a single non-relationship field has no type, the entire update-record step fails — even when the AI would have picked another, well-typed field.
That's exactly the kind of global fragility this PR removes on the schema side ("never in bulk up front for an unrelated add/remove"). A single drifted, typeless field blocks update-record for the whole collection.
The override path (coerceFieldValue) already gets this right: it only guards the targeted field. Two ways to align the AI path:
- filter typeless fields out of the choices offered to the AI —
nonRelationFields.filter(f => f.type != null)— and only throw when the selected field is resolved; - or move the guard to the point where the targeted field is resolved, rather than at global schema-build time.
…pdate step Review feedback (matthv): buildUpdateFieldTool maps over all non-relationship fields, so FieldTypeMissingError thrown by buildZodSchemaForField for a single drifted type-less field failed the entire update-record step — the global fragility this PR removes on the schema side. Exclude type-less fields from the choices offered to the AI (filter f.type != null). The AI only sees updatable fields; if none are updatable, NoWritableFieldsError. The override path still rejects an explicit type-less target via FieldTypeMissingError. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Problem
The orchestrator deploys independently and evolves the collection schema it returns (e.g. adds
relatedPrimaryKeyper field,referenceFieldat root — from PRD-360 / load-related-record). With.strict(), any additive change made the whole step fail at parse:…even though the step never uses those fields.
Approach (resilience, Niveau 2)
Only fail when a step genuinely lacks what it needs, at execution — never in bulk up front for an unrelated add/remove.
CollectionSchema+ nestedFieldSchema/ActionSchema/ActionHooks) → additive drift tolerated.actions→.optional().default([])— only trigger-action uses it (NoActionsErrorwhen empty).type→.nullable().optional()— only update-record uses it (buildZodSchemaForFieldfalls back to a string schema).collectionName,primaryKeyFields,fields,fieldName,displayName,isRelationship).AvailableStepExecution, reconstructedStepUser/Step/RecordRef) and frontend HTTP bodies (pending-data-validators.ts) keep.strict().step-definition.tsalready stripped.Notes
CollectionSchemavalidation already runs during execution → its failure surfaces as a step error, not a global malformed. No bucketing change.DomainValidationErrormessage always says "mapper produced invalid AvailableStepExecution" even whengetCollectionSchemais the failing parse — misleading.Tests
referenceFieldroot +relatedPrimaryKeyfield absent from result).actions→[]; field withouttype→ parses. Kept the required-field guard (field withoutfieldNamestill throws).🤖 Generated with Claude Code
Note
Tolerate orchestrator collection-schema drift in workflow-executor validation
.strict()from collection schema validators incollection.ts, stripping unknown keys instead of rejecting them, so orchestrator schema changes don't break workflow execution.typeoptional/nullable onFieldSchemaSchemaand defaultsactionsto[]when omitted, accommodating fields the orchestrator may not fully populate.FieldTypeMissingErrorand updatesbuildZodSchemaForFieldandcoerceFieldValueinupdate-record-step-executor.tsto fail fast with a user-facing error when a non-relationship field lacks a type, rather than silently defaulting to string coercion.Changes since #1613 opened
UpdateRecordStepExecutor.buildUpdateFieldToolmethod inworkflow-executorto filter out non-relationship fields that lack a type property [5f4b755]workflow-executorto verify orchestrator drift tolerance [5f4b755]Macroscope summarized 57bf3a1.