Skip to content

fix(workflow-executor): preserve AI suggestion in pending data#1586

Merged
Tonours merged 18 commits into
feat/prd-214-server-step-mapperfrom
fix/workflow-executor-preserve-ai-suggestion-in-pending-data
May 26, 2026
Merged

fix(workflow-executor): preserve AI suggestion in pending data#1586
Tonours merged 18 commits into
feat/prd-214-server-step-mapperfrom
fix/workflow-executor-preserve-ai-suggestion-in-pending-data

Conversation

@Tonours
Copy link
Copy Markdown
Member

@Tonours Tonours commented May 19, 2026

Summary

  • Keeps pendingData immutable so the AI proposal can be diffed against the user's final choice. User overrides now land in a sibling userConfirmation: Record<string, unknown> populated by patchAndReloadPendingData from the validated PATCH body.
  • update-record, trigger-action and load-related-record executors read user-confirmed values (value, actionResult, name/displayName/selectedRecordId) from userConfirmation with fallback to the AI suggestion in pendingData.
  • userConfirmation is replaced last-write-wins (no spread-merge) to avoid stale keys bleeding across sequential PATCHes.

Test plan

  • yarn workspace @forestadmin/workflow-executor build
  • yarn workspace @forestadmin/workflow-executor test (726 tests pass)
  • yarn workspace @forestadmin/workflow-executor lint
  • Regression tests added: AI suggestion preserved with user-override of value / selectedRecordId / relation name+displayName, accept-via-PATCH without override (fallback), rejection via PATCH with userConfirmation set.

@qltysh
Copy link
Copy Markdown

qltysh Bot commented May 19, 2026

Qlty


Coverage Impact

This PR will not change total coverage.

Modified Files with Diff Coverage (5)

RatingFile% DiffUncovered Line #s
Coverage rating: A Coverage rating: A
...ow-executor/src/executors/load-related-record-step-executor.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/http/pending-data-validators.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/executors/base-step-executor.ts100.0%
Coverage rating: A Coverage rating: A
...workflow-executor/src/executors/update-record-step-executor.ts100.0%
Coverage rating: A Coverage rating: A
...-executor/src/executors/trigger-record-action-step-executor.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.

@Tonours Tonours force-pushed the fix/workflow-executor-preserve-ai-suggestion-in-pending-data branch from c797768 to 7966b7f Compare May 19, 2026 19:22
@Tonours Tonours force-pushed the fix/workflow-executor-preserve-ai-suggestion-in-pending-data branch from 7966b7f to d58877a Compare May 19, 2026 19:30
Comment thread packages/workflow-executor/src/executors/base-step-executor.ts
Comment on lines +151 to +153
const isString = (v: unknown): v is string => typeof v === 'string';
const isRecordId = (v: unknown): v is Array<string | number> =>
Array.isArray(v) && v.every(e => typeof e === 'string' || typeof e === 'number');
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.

Data are already validated by zod when we receive the http request. I think we don't need to validate two times

// Re-derive relatedCollectionName from schema using the (possibly updated) relation name.
// `name` is always a fieldName (set from field.fieldName in buildTarget) — search directly.
const name = isString(userConfirmation?.name) ? userConfirmation.name : pendingData.name;
const displayName = isString(userConfirmation?.displayName)
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.

displayName seems fully derivable from the schema once we have name, so could we remove it from the HTTP payload entirely? That would avoid having to keep name / displayName in sync and reduce the risk of persisting an inconsistent state.

More generally, it feels like the invariant here should be:

changing only selectedRecordId while keeping the same name is valid,
but if name changes, then selectedRecordId should also be required,
and displayName should be recomputed from the schema rather than trusted from the client.
So maybe we should tighten this in two places:

in the Zod schema, remove displayName and require selectedRecordId whenever name is provided,
in the executor, use name as the source of truth and recompute displayName from the resolved field/schema.

Copy link
Copy Markdown
Member

@Scra3 Scra3 left a comment

Choose a reason for hiding this comment

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

Two blocking type-safety issues need to be addressed before merge.

  • WithUserConfirmation.userConfirmation is typed as Record<string, unknown>, erasing the per-step shape that Zod already validated — executors end up re-implementing validation with runtime guards.
  • McpStepExecutionData (line 106 in step-execution-data.ts) is the only executor that calls patchAndReloadPendingData but does not extend WithUserConfirmation — the userConfirmation field will be set at runtime but is invisible to TypeScript, breaking the promise of the PR.

Fix: export one inferred type per Zod schema in pending-data-validators.ts (e.g. export type LoadRelatedRecordConfirmation = z.infer<typeof loadRelatedRecordPatchSchema>) and use those precise types in the interfaces. The runtime guards in resolveFromSelection can then be dropped.

// Parsed PATCH body kept beside `pendingData` so executors can read the user's
// final input without overwriting the AI suggestion.
export interface WithUserConfirmation {
userConfirmation?: Record<string, unknown>;
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.

issue (blocking): Record<string, unknown> erases the per-step shape that Zod already validated — executors must then add runtime type guards for fields the schema already knows. Export one inferred type per schema in pending-data-validators.ts and use them here per step instead of this catch-all.

);
}

// Keeps `pendingData` immutable; mirrors `userConfirmed` only because
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.

nit: "immutable" is misleading — userConfirmed is written back into pendingData four lines below. The intent is that the original AI suggestion is preserved (not overwritten by a full merge), which is different from immutability.

}

const { name, displayName, selectedRecordId } = pendingData;
const isString = (v: unknown): v is string => typeof v === 'string';
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.

issue (non-blocking): these guards exist only because userConfirmation is typed as Record<string, unknown> — Zod already validated that name is a string and selectedRecordId is Array<string | number>. Fix the WithUserConfirmation typing and both guards can be dropped in favour of direct property access.

// Re-derive relatedCollectionName from schema using the (possibly updated) relation name.
// `name` is always a fieldName (set from field.fieldName in buildTarget) — search directly.
const name = isString(userConfirmation?.name) ? userConfirmation.name : pendingData.name;
const displayName = isString(userConfirmation?.displayName)
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.

suggestion: displayName can be re-derived from the schema once name is resolved (field.displayName), so there is no need to accept it from the PATCH body — trusting client-provided display names risks surfacing stale or inconsistent data.

Comment thread packages/workflow-executor/src/http/pending-data-validators.ts Outdated
@Scra3
Copy link
Copy Markdown
Member

Scra3 commented May 20, 2026

@Scra3
Copy link
Copy Markdown
Member

Scra3 commented May 20, 2026

linked to: PRD-378

@Scra3 Scra3 force-pushed the fix/workflow-executor-preserve-ai-suggestion-in-pending-data branch from 96808c0 to 13499c5 Compare May 25, 2026 15:32
@Scra3 Scra3 changed the base branch from feat/prd-214-server-step-mapper to fix/execution-type May 25, 2026 15:33
@qltysh
Copy link
Copy Markdown

qltysh Bot commented May 25, 2026

All good ✅

Base automatically changed from fix/execution-type to feat/prd-214-server-step-mapper May 25, 2026 20:06
@Scra3 Scra3 force-pushed the fix/workflow-executor-preserve-ai-suggestion-in-pending-data branch from 64c7f81 to 6862dfc Compare May 25, 2026 20:16
Comment thread packages/workflow-executor/src/cli-core.ts Outdated
Tonours and others added 12 commits May 26, 2026 09:57
… and re-derive displayName from schema

- Export named Zod schemas and inferred types from pending-data-validators (UpdateRecordConfirmation, TriggerActionConfirmation, McpConfirmation, LoadRelatedRecordConfirmation)
- Make WithUserConfirmation generic so each interface carries the exact confirmation shape instead of Record<string, unknown>
- Add WithUserConfirmation<McpConfirmation> to McpStepExecutionData (was missing despite mcp executor writing userConfirmation)
- Remove isString/isRecordId runtime guards in resolveFromSelection — now unnecessary with precise typing
- Re-derive displayName from FieldSchema instead of accepting it from the PATCH body; remove displayName from loadRelatedRecordPatchSchema and contract
- Fix inaccurate comment "Keeps pendingData immutable" in patchAndReloadPendingData

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… in load-related-record patch

Sending a different relation name without a new record ID would silently reuse the AI-suggested
record ID from the original collection, producing a wrong or non-existent record in the new
relation. Zod refine now enforces that selectedRecordId is required when name is overridden.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ata and validators test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tion for load-related-record

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…kup, warn on timeout discard, fix logStartup default, tighten key type

- Replace non-null assertion on patchBodySchemas lookup with an explicit StepStateError guard
- Downgrade post-timeout discard log from info to warn (correct severity)
- Fix logStartup reporting undefined for pollingIntervalMs when env var is unset
- Narrow patchBodySchemas key type from string to PatchableStepType union
- Remove unused GuidanceConfirmation export

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…step type in patchAndReloadPendingData

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…onfirmableStepExecutionData union

- Export ConfirmableStepExecutionData (explicit union of the 4 step types that use
  the confirmation flow) from step-execution-data.ts
- Drop local WithPendingData intersection type — it re-declared constraints already
  present in the concrete types
- Drop PatchBody cast — cast directly to TExec['userConfirmation'] after safeParse

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ution-data

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…date-record confirmation flow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Scra3 Scra3 force-pushed the fix/workflow-executor-preserve-ai-suggestion-in-pending-data branch from 6862dfc to d3f3a5e Compare May 26, 2026 08:01
alban bertolini and others added 6 commits May 26, 2026 10:14
…pollingIntervalMs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…serConfirmation

pendingData now holds only the AI suggestion. handleConfirmationFlow reads
userConfirmed from userConfirmation instead of pendingData.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ion terminology

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ation split

- Section 3 rewritten: pendingData = AI suggestion (never overwritten),
  userConfirmation = validated POST body stored separately
- trigger-action and mcp split into separate blocks; actionResult documented
- Remove stale userConfirmed from RunStore pendingData shapes
- Fix "PATCH" → "POST" in two comments

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ecord pendingData

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Tonours Tonours merged commit f254266 into feat/prd-214-server-step-mapper May 26, 2026
49 of 57 checks passed
@Tonours Tonours deleted the fix/workflow-executor-preserve-ai-suggestion-in-pending-data branch May 26, 2026 13:05
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