Skip to content

fix(workflow-executor): tolerate orchestrator collection-schema drift#1613

Merged
Scra3 merged 3 commits into
feat/prd-214-server-step-mapperfrom
fix/executor-schema-resilience
Jun 2, 2026
Merged

fix(workflow-executor): tolerate orchestrator collection-schema drift#1613
Scra3 merged 3 commits into
feat/prd-214-server-step-mapperfrom
fix/executor-schema-resilience

Conversation

@Scra3
Copy link
Copy Markdown
Member

@Scra3 Scra3 commented Jun 1, 2026

Problem

The orchestrator deploys independently and evolves the collection schema it returns (e.g. adds relatedPrimaryKey per field, referenceField at root — from PRD-360 / load-related-record). With .strict(), any additive change made the whole step fail at parse:

Run 244 ... fields.1: Unrecognized key: "relatedPrimaryKey"; (root): Unrecognized key: "referenceField"

…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.

  • Strip unknown keys on the orchestrator collection schema (CollectionSchema + nested FieldSchema/ActionSchema/ActionHooks) → additive drift tolerated.
  • Required minimal: demote step-specific props that already have a use-time guard:
    • actions.optional().default([]) — only trigger-action uses it (NoActionsError when empty).
    • field type.nullable().optional() — only update-record uses it (buildZodSchemaForField falls back to a string schema).
  • Structural fields stay required (collectionName, primaryKeyFields, fields, fieldName, displayName, isRelationship).
  • Unchanged: produced/envelope schemas (AvailableStepExecution, reconstructed StepUser/Step/RecordRef) and frontend HTTP bodies (pending-data-validators.ts) keep .strict(). step-definition.ts already stripped.

Notes

  • CollectionSchema validation already runs during execution → its failure surfaces as a step error, not a global malformed. No bucketing change.
  • CLAUDE.md "Boundary validation" rule amended accordingly.
  • Out of scope (follow-up): DomainValidationError message always says "mapper produced invalid AvailableStepExecution" even when getCollectionSchema is the failing parse — misleading.

Tests

  • Updated the strict-rejection test → asserts unknown keys are stripped (referenceField root + relatedPrimaryKey field absent from result).
  • Added: omitted actions[]; field without type → parses. Kept the required-field guard (field without fieldName still throws).
  • Full suite: 835/835. Lint: 0 errors.

🤖 Generated with Claude Code

Note

Tolerate orchestrator collection-schema drift in workflow-executor validation

  • Removes .strict() from collection schema validators in collection.ts, stripping unknown keys instead of rejecting them, so orchestrator schema changes don't break workflow execution.
  • Makes type optional/nullable on FieldSchemaSchema and defaults actions to [] when omitted, accommodating fields the orchestrator may not fully populate.
  • Adds FieldTypeMissingError and updates buildZodSchemaForField and coerceFieldValue in update-record-step-executor.ts to fail fast with a user-facing error when a non-relationship field lacks a type, rather than silently defaulting to string coercion.
  • Behavioral Change: typeless non-relationship fields now cause an explicit error in both the AI input schema path and the override coercion path, instead of being silently treated as strings.

Changes since #1613 opened

  • Modified UpdateRecordStepExecutor.buildUpdateFieldTool method in workflow-executor to filter out non-relationship fields that lack a type property [5f4b755]
  • Updated tests for type-less field handling in workflow-executor to verify orchestrator drift tolerance [5f4b755]

Macroscope summarized 57bf3a1.

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>
@qltysh
Copy link
Copy Markdown

qltysh Bot commented Jun 1, 2026

Qlty


Coverage Impact

Unable to calculate total coverage change because base branch coverage was not found.

Modified Files with Diff Coverage (3)

RatingFile% DiffUncovered Line #s
New Coverage rating: A
...workflow-executor/src/executors/update-record-step-executor.ts100.0%
New Coverage rating: A
packages/workflow-executor/src/errors.ts100.0%
New Coverage rating: A
packages/workflow-executor/src/types/validated/collection.ts100.0%
Total100.0%
🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

…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>
@qltysh
Copy link
Copy Markdown

qltysh Bot commented Jun 1, 2026

1 new issue

Tool Category Rule Count
qlty Structure Function with many returns (count = 4): buildZodSchemaForField 1

z.object({
fieldName: z.literal(f.displayName),
value: buildZodSchemaForField(f).nullable(),
value: buildZodSchemaForField(f, schema.collectionName).nullable(),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@Scra3 Scra3 merged commit 317ed0b into feat/prd-214-server-step-mapper Jun 2, 2026
51 of 57 checks passed
@Scra3 Scra3 deleted the fix/executor-schema-resilience branch June 2, 2026 07:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants