fix(workflow-executor): use last step from history instead of scanning for first non-done#1581
Conversation
## @forestadmin/ai-proxy [1.7.3](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/ai-proxy@1.7.2...@forestadmin/ai-proxy@1.7.3) (2026-04-08) ### Bug Fixes * **ai-proxy:** update zendesk tools functionality ([#1533](#1533)) ([e29e63e](e29e63e))
## @forestadmin/forestadmin-client [1.38.3](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/forestadmin-client@1.38.2...@forestadmin/forestadmin-client@1.38.3) (2026-04-08) ### Dependencies * **@forestadmin/ai-proxy:** upgraded to 1.7.3
## @forestadmin/agent-client [1.4.19](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/agent-client@1.4.18...@forestadmin/agent-client@1.4.19) (2026-04-08) ### Dependencies * **@forestadmin/forestadmin-client:** upgraded to 1.38.3
## @forestadmin/mcp-server [1.8.14](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/mcp-server@1.8.13...@forestadmin/mcp-server@1.8.14) (2026-04-08) ### Dependencies * **@forestadmin/agent-client:** upgraded to 1.4.19 * **@forestadmin/forestadmin-client:** upgraded to 1.38.3
## @forestadmin/agent [1.76.3](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/agent@1.76.2...@forestadmin/agent@1.76.3) (2026-04-08) ### Dependencies * **@forestadmin/forestadmin-client:** upgraded to 1.38.3 * **@forestadmin/mcp-server:** upgraded to 1.8.14
## @forestadmin/agent-testing [1.1.3](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/agent-testing@1.1.2...@forestadmin/agent-testing@1.1.3) (2026-04-08) ### Dependencies * **@forestadmin/agent-client:** upgraded to 1.4.19 * **@forestadmin/forestadmin-client:** upgraded to 1.38.3 * **@forestadmin/agent:** upgraded to 1.76.3
## @forestadmin/forest-cloud [1.12.104](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/forest-cloud@1.12.103...@forestadmin/forest-cloud@1.12.104) (2026-04-08) ### Dependencies * **@forestadmin/agent:** upgraded to 1.76.3
## @forestadmin/agent-client [1.4.20](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/agent-client@1.4.19...@forestadmin/agent-client@1.4.20) (2026-04-10) ### Bug Fixes * **mcp-server:** add snake_case JWT claims for Ruby backend compatibility ([#1538](#1538)) ([b3b7dcd](b3b7dcd))
## @forestadmin/mcp-server [1.8.15](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/mcp-server@1.8.14...@forestadmin/mcp-server@1.8.15) (2026-04-10) ### Bug Fixes * **mcp-server:** add snake_case JWT claims for Ruby backend compatibility ([#1538](#1538)) ([b3b7dcd](b3b7dcd)) ### Dependencies * **@forestadmin/agent-client:** upgraded to 1.4.20
## @forestadmin/agent [1.76.4](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/agent@1.76.3...@forestadmin/agent@1.76.4) (2026-04-10) ### Dependencies * **@forestadmin/mcp-server:** upgraded to 1.8.15
## @forestadmin/agent-testing [1.1.4](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/agent-testing@1.1.3...@forestadmin/agent-testing@1.1.4) (2026-04-10) ### Dependencies * **@forestadmin/agent-client:** upgraded to 1.4.20 * **@forestadmin/agent:** upgraded to 1.76.4
## @forestadmin/forest-cloud [1.12.105](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/forest-cloud@1.12.104...@forestadmin/forest-cloud@1.12.105) (2026-04-10) ### Dependencies * **@forestadmin/agent:** upgraded to 1.76.4
Co-authored-by: alban bertolini <albanb@forestadmin.com> Co-authored-by: Nicolas Bouliol <nicolas.bouliol@forestadmin.com>
## @forestadmin/forestadmin-client [1.38.4](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/forestadmin-client@1.38.3...@forestadmin/forestadmin-client@1.38.4) (2026-04-10) ### Bug Fixes * **mcp-server:** action execution on v1 agent ([#1542](#1542)) ([01b1a64](01b1a64))
## @forestadmin/agent-client [1.4.21](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/agent-client@1.4.20...@forestadmin/agent-client@1.4.21) (2026-04-10) ### Bug Fixes * **mcp-server:** action execution on v1 agent ([#1542](#1542)) ([01b1a64](01b1a64)) ### Dependencies * **@forestadmin/forestadmin-client:** upgraded to 1.38.4
## @forestadmin/mcp-server [1.8.16](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/mcp-server@1.8.15...@forestadmin/mcp-server@1.8.16) (2026-04-10) ### Bug Fixes * **mcp-server:** action execution on v1 agent ([#1542](#1542)) ([01b1a64](01b1a64)) ### Dependencies * **@forestadmin/agent-client:** upgraded to 1.4.21 * **@forestadmin/forestadmin-client:** upgraded to 1.38.4
## @forestadmin/agent [1.76.5](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/agent@1.76.4...@forestadmin/agent@1.76.5) (2026-04-10) ### Dependencies * **@forestadmin/forestadmin-client:** upgraded to 1.38.4 * **@forestadmin/mcp-server:** upgraded to 1.8.16
## @forestadmin/agent-testing [1.1.5](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/agent-testing@1.1.4...@forestadmin/agent-testing@1.1.5) (2026-04-10) ### Bug Fixes * **mcp-server:** action execution on v1 agent ([#1542](#1542)) ([01b1a64](01b1a64)) ### Dependencies * **@forestadmin/agent-client:** upgraded to 1.4.21 * **@forestadmin/forestadmin-client:** upgraded to 1.38.4 * **@forestadmin/agent:** upgraded to 1.76.5
## @forestadmin/forest-cloud [1.12.106](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/forest-cloud@1.12.105...@forestadmin/forest-cloud@1.12.106) (2026-04-10) ### Dependencies * **@forestadmin/agent:** upgraded to 1.76.5
…mpatibility (#1544) Co-authored-by: alban bertolini <albanb@forestadmin.com> Co-authored-by: Nicolas Bouliol <nicolas.bouliol@forestadmin.com>
## @forestadmin/agent-client [1.4.22](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/agent-client@1.4.21...@forestadmin/agent-client@1.4.22) (2026-04-10) ### Bug Fixes * **agent-client:** convert filter operators to snake_case for Ruby compatibility ([#1544](#1544)) ([00e8ad5](00e8ad5))
## @forestadmin/mcp-server [1.8.17](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/mcp-server@1.8.16...@forestadmin/mcp-server@1.8.17) (2026-04-10) ### Bug Fixes * **agent-client:** convert filter operators to snake_case for Ruby compatibility ([#1544](#1544)) ([00e8ad5](00e8ad5)) ### Dependencies * **@forestadmin/agent-client:** upgraded to 1.4.22
## @forestadmin/agent [1.76.6](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/agent@1.76.5...@forestadmin/agent@1.76.6) (2026-04-10) ### Dependencies * **@forestadmin/mcp-server:** upgraded to 1.8.17
## @forestadmin/agent-testing [1.1.6](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/agent-testing@1.1.5...@forestadmin/agent-testing@1.1.6) (2026-04-10) ### Dependencies * **@forestadmin/agent-client:** upgraded to 1.4.22 * **@forestadmin/agent:** upgraded to 1.76.6
## @forestadmin/forest-cloud [1.12.107](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/forest-cloud@1.12.106...@forestadmin/forest-cloud@1.12.107) (2026-04-10) ### Dependencies * **@forestadmin/agent:** upgraded to 1.76.6
# @forestadmin/mcp-server [1.9.0](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/mcp-server@1.8.17...@forestadmin/mcp-server@1.9.0) (2026-04-13) ### Features * **mcp-server:** add disabledTools option to ForestMCPServerOptions ([#1543](#1543)) ([d36ad10](d36ad10))
…TCH body
The server validator (Joi) only accepts { status } — extra fields cause a
400 ValidationFailedError. The error message is still logged locally via
stepErrorMessage on failure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nt camelCase deserialization The JSON:API deserializer in agent-client converts all attribute keys to camelCase (card_number → cardNumber). ReadRecordStepExecutor looks up values by original field name, so snake_case fields came back undefined and were stripped from executionResult. Add restoreFieldNames() in AgentClientAgentPort.getRecord to reverse the camelCase mapping using the original field names from the query, ensuring executors receive values keyed by the field name they requested. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…d and values Same camelCase deserialization issue as getRecord: apply restoreFieldNames to related records using both primaryKeyFields and requested fields so that extractRecordId finds the correct PK values and callers receive snake_case keys. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
## @forestadmin/forestadmin-client [1.39.5](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/forestadmin-client@1.39.4...@forestadmin/forestadmin-client@1.39.5) (2026-04-30) ### Bug Fixes * **activity-logs:** remove errorMessage from updateActivityLogStatus ([#1576](#1576)) ([19585fc](19585fc))
## @forestadmin/agent-client [1.5.6](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/agent-client@1.5.5...@forestadmin/agent-client@1.5.6) (2026-04-30) ### Dependencies * **@forestadmin/forestadmin-client:** upgraded to 1.39.5
## @forestadmin/mcp-server [1.11.7](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/mcp-server@1.11.6...@forestadmin/mcp-server@1.11.7) (2026-04-30) ### Bug Fixes * **activity-logs:** remove errorMessage from updateActivityLogStatus ([#1576](#1576)) ([19585fc](19585fc)) ### Dependencies * **@forestadmin/agent-client:** upgraded to 1.5.6 * **@forestadmin/forestadmin-client:** upgraded to 1.39.5
## @forestadmin/agent [1.78.8](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/agent@1.78.7...@forestadmin/agent@1.78.8) (2026-04-30) ### Bug Fixes * **logger:** add logger in case of start failure ([#1572](#1572)) ([f6a90c6](f6a90c6)) ### Dependencies * **@forestadmin/forestadmin-client:** upgraded to 1.39.5 * **@forestadmin/mcp-server:** upgraded to 1.11.7
## @forestadmin/agent-testing [1.1.18](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/agent-testing@1.1.17...@forestadmin/agent-testing@1.1.18) (2026-04-30) ### Dependencies * **@forestadmin/agent-client:** upgraded to 1.5.6 * **@forestadmin/forestadmin-client:** upgraded to 1.39.5 * **@forestadmin/agent:** upgraded to 1.78.8
## @forestadmin/forest-cloud [1.12.119](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/forest-cloud@1.12.118...@forestadmin/forest-cloud@1.12.119) (2026-04-30) ### Dependencies * **@forestadmin/agent:** upgraded to 1.78.8
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lues Same camelCase deserialization issue as getRecord/getRelatedData: agent-client's JSON:API deserializer converts response keys to camelCase. Restore original names using the input values keys, which are already in the caller's original format. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…client to 1.39.5 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…pending data When the executor picks up a guidance step from the polling loop (before the user has submitted anything), it now returns awaiting-input instead of throwing StepStateError. This keeps the step pending so the user trigger can process it with the submitted data. Also makes userInput optional so users can submit a guidance step without providing any text input. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…restoreFieldNames Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… rethrow path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ect to fix OpenAI rejection
OpenAI requires type: "object" at the root of a tool schema. Using
z.discriminatedUnion directly as schema serialized to anyOf, causing
a 400 "got type: None" error on update-data steps with ≥2 fields.
Wrap in z.object({ input: ... }) (same pattern as the frontend) so
the root is always type: "object". Switch to z.union for the field
variants — discriminatedUnion brought no benefit for LLM selection.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… root schema shape Add two tests to prevent the OpenAI 400 regression from silently re-appearing: one asserting the root schema is a ZodObject (not a union), and one covering the multi-field z.union path with a flat payload rejection check. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…apper for update-record-field
The integration tests were still passing the old flat args shape
{ fieldName, value, reasoning } to the mock model. After wrapping
the tool schema in z.object({ input: ... }), the executor now
destructures result.input — so the mocks need the wrapper too.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…r) when finding pending step After a back change, errored steps are stored as done:false + context.error instead of done:true. The find() must exclude them so the executor picks the next genuinely pending step and not the already-failed one. Also includes a temporary throw in ReadRecordStepExecutor for manual front-end error testing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… first non-done The orchestrator is the source of truth for which step to execute next. Always pick the last entry in workflowHistory rather than scanning for the first non-done/non-cancelled/non-errored step. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
22 new issues
|
|
Closing duplicate — commit already covered by #1564 |
There was a problem hiding this comment.
🟢 Low
Adding 'BelongsToMany' to RelationTarget.relationType without updating resolveAndLoadAutomatic causes BelongsToMany relations to fall through to fetchFirstCandidate, which returns an arbitrary first record. Since BelongsToMany is a many-to-many relationship with multiple candidates, this bypasses the AI-based selection that HasMany receives via selectBestRelatedRecord.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/workflow-executor/src/executors/load-related-record-step-executor.ts around line 132:
Adding `'BelongsToMany'` to `RelationTarget.relationType` without updating `resolveAndLoadAutomatic` causes `BelongsToMany` relations to fall through to `fetchFirstCandidate`, which returns an arbitrary first record. Since `BelongsToMany` is a many-to-many relationship with multiple candidates, this bypasses the AI-based selection that `HasMany` receives via `selectBestRelatedRecord`.
Evidence trail:
packages/workflow-executor/src/executors/load-related-record-step-executor.ts:36 (RelationTarget type includes BelongsToMany), lines 132-136 (resolveAndLoadAutomatic only checks HasMany), lines 239-241 (fetchFirstCandidate fetches limit=1), lines 232-235 (selectBestRelatedRecord uses AI with 50 records). PR diff confirms BelongsToMany was added in this PR to line 36 but resolveAndLoadAutomatic was not updated. packages/workflow-executor/src/types/validated/collection.ts:42 confirms BelongsToMany is a valid schema relationType.
| function buildZodSchemaForField(field: FieldSchema): z.ZodTypeAny { | ||
| const { type, enumValues } = field; | ||
|
|
||
| if (Array.isArray(type)) { | ||
| // Nested array (e.g. [['String']]) → treat as opaque JSON. | ||
| if (Array.isArray(type[0])) return z.array(jsonStringSchema); | ||
|
|
||
| return z.array(buildZodSchemaForPrimitive(type[0] as string, enumValues)); | ||
| } | ||
|
|
||
| if (typeof type === 'object' && type !== null) { | ||
| return jsonStringSchema; | ||
| } | ||
|
|
||
| return buildZodSchemaForPrimitive(type as string, enumValues); | ||
| } |
There was a problem hiding this comment.
🟢 Low executors/update-record-step-executor.ts:76
In buildZodSchemaForField, when type is an array of JSON object structures like [{ field: 'String' }], the check at line 81 only handles nested arrays, not nested objects. Array.isArray(type[0]) is false for objects, so line 83 passes the object to buildZodSchemaForPrimitive as a string, causing it to fall through to the default case and return z.string(). This validates arrays of structured JSON as arrays of plain strings instead of treating them as opaque JSON.
-function buildZodSchemaForField(field: FieldSchema): z.ZodTypeAny {
- const { type, enumValues } = field;
-
- if (Array.isArray(type)) {
- // Nested array (e.g. [['String']]) → treat as opaque JSON.
- if (Array.isArray(type[0])) return z.array(jsonStringSchema);
-
- return z.array(buildZodSchemaForPrimitive(type[0] as string, enumValues));
- }
-
- if (typeof type === 'object' && type !== null) {
- return jsonStringSchema;
- }
-
- return buildZodSchemaForPrimitive(type as string, enumValues);
-}🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/workflow-executor/src/executors/update-record-step-executor.ts around lines 76-91:
In `buildZodSchemaForField`, when `type` is an array of JSON object structures like `[{ field: 'String' }]`, the check at line 81 only handles nested arrays, not nested objects. `Array.isArray(type[0])` is false for objects, so line 83 passes the object to `buildZodSchemaForPrimitive` as a string, causing it to fall through to the default case and return `z.string()`. This validates arrays of structured JSON as arrays of plain strings instead of treating them as opaque JSON.
Evidence trail:
packages/workflow-executor/src/types/validated/collection.ts lines 26-33 (ColumnType definition showing `[{field:'String'}]` is valid), packages/workflow-executor/src/executors/update-record-step-executor.ts lines 76-91 (`buildZodSchemaForField` missing object-in-array handling), lines 42-74 (`buildZodSchemaForPrimitive` switch defaulting to `z.string()` for non-string input)
| pollingIntervalMs: parsePositiveIntEnv('POLLING_INTERVAL_MS', env.POLLING_INTERVAL_MS), | ||
| stopTimeoutMs: parsePositiveIntEnv('STOP_TIMEOUT_MS', env.STOP_TIMEOUT_MS), | ||
| stepTimeoutMs: parsePositiveIntEnv('STEP_TIMEOUT_MS', env.STEP_TIMEOUT_MS), | ||
| maxChainDepth: parsePositiveIntEnv('MAX_CHAIN_DEPTH', env.MAX_CHAIN_DEPTH), |
There was a problem hiding this comment.
🟠 High src/cli-core.ts:153
maxChainDepth is parsed with parsePositiveIntEnv, which uses z.coerce.number().int().positive() and rejects 0. According to the interface comment in ExecutorOptions, 0 is a valid value that disables chaining, so users setting MAX_CHAIN_DEPTH=0 receive a validation error instead of disabling chaining. Consider using a non-negative integer parser for this field.
- maxChainDepth: parsePositiveIntEnv('MAX_CHAIN_DEPTH', env.MAX_CHAIN_DEPTH),
+ maxChainDepth: parseNonNegativeIntEnv('MAX_CHAIN_DEPTH', env.MAX_CHAIN_DEPTH),🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/workflow-executor/src/cli-core.ts around line 153:
`maxChainDepth` is parsed with `parsePositiveIntEnv`, which uses `z.coerce.number().int().positive()` and rejects `0`. According to the interface comment in `ExecutorOptions`, `0` is a valid value that disables chaining, so users setting `MAX_CHAIN_DEPTH=0` receive a validation error instead of disabling chaining. Consider using a non-negative integer parser for this field.
Evidence trail:
packages/workflow-executor/src/cli-core.ts:17 - `const POSITIVE_INT = z.coerce.number().int().positive();`
packages/workflow-executor/src/cli-core.ts:19-29 - `parsePositiveIntEnv` function using POSITIVE_INT
packages/workflow-executor/src/cli-core.ts:153 - `maxChainDepth: parsePositiveIntEnv('MAX_CHAIN_DEPTH', env.MAX_CHAIN_DEPTH)`
packages/workflow-executor/src/build-workflow-executor.ts:43-44 - `// Max auto-chained steps per entry (see RunnerConfig.maxChainDepth). 0 disables chaining.` and `maxChainDepth?: number;`
packages/workflow-executor/test/runner.test.ts:897-910 - test 'disables chaining when maxChainDepth is 0' using `maxChainDepth: 0`
| ): Promise<StepExecutionResult> { | ||
| const { selectedRecordRef, displayName, name } = target; | ||
|
|
||
| await this.context.runStore.saveStepExecution(this.context.runId, { |
There was a problem hiding this comment.
🟢 Low executors/trigger-record-action-step-executor.ts:182
saveFrontendResult persists the execution result but never sets idempotencyPhase: 'done'. When checkIdempotency runs on a frontend-completed step, existing?.idempotencyPhase === 'done' is false, so the check returns null instead of short-circuiting with success. This allows the step to be re-executed on retry, unlike executeOnExecutor which correctly sets the phase.
Also found in 1 other location(s)
packages/workflow-executor/src/executors/update-record-step-executor.ts:228
If
saveStepExecutionfails afterupdateRecordsucceeds at line 228-236, the record is updated in the database but the idempotency phase remains 'executing'. On retry,checkIdempotency()at lines 118-119 sees 'executing' and throwsStepStateError, telling the user to retry manually. However, the update already succeeded, so a manual retry would update the record again, causing duplicate updates. The previous code wrapped this in a try-catch withStepPersistenceErrorthat explicitly stated 'Record update persisted but step state could not be saved', giving context that the update succeeded despite the save failure.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/workflow-executor/src/executors/trigger-record-action-step-executor.ts around line 182:
`saveFrontendResult` persists the execution result but never sets `idempotencyPhase: 'done'`. When `checkIdempotency` runs on a frontend-completed step, `existing?.idempotencyPhase === 'done'` is false, so the check returns `null` instead of short-circuiting with success. This allows the step to be re-executed on retry, unlike `executeOnExecutor` which correctly sets the phase.
Evidence trail:
packages/workflow-executor/src/executors/trigger-record-action-step-executor.ts lines 45-59 (checkIdempotency), lines 132-137 (Branch C initial save, no idempotencyPhase), lines 143-172 (executeOnExecutor sets idempotencyPhase: 'done' at line 168), lines 175-192 (saveFrontendResult spreads existingExecution but never adds idempotencyPhase: 'done'). packages/workflow-executor/src/executors/base-step-executor.ts lines 248-272 (handleConfirmationFlow does not set idempotencyPhase). packages/workflow-executor/src/types/step-execution-data.ts lines 13-14 (MutatingStepExecutionData defines optional idempotencyPhase).
Also found in 1 other location(s):
- packages/workflow-executor/src/executors/update-record-step-executor.ts:228 -- If `saveStepExecution` fails after `updateRecord` succeeds at line 228-236, the record is updated in the database but the idempotency phase remains 'executing'. On retry, `checkIdempotency()` at lines 118-119 sees 'executing' and throws `StepStateError`, telling the user to retry manually. However, the update already succeeded, so a manual retry would update the record again, causing duplicate updates. The previous code wrapped this in a try-catch with `StepPersistenceError` that explicitly stated 'Record update persisted but step state could not be saved', giving context that the update succeeded despite the save failure.
| if (outcomeType === 'guidance') { | ||
| const status: GuidanceStepOutcome['status'] = ctx.status === 'error' ? 'error' : 'success'; | ||
|
|
||
| return { type: 'guidance', ...baseFromCtx, status }; | ||
| } |
There was a problem hiding this comment.
🟢 Low adapters/run-to-available-step-mapper.ts:63
The guidance branch at line 64 only checks for 'error' status, mapping any other value (including 'awaiting-input') to 'success'. This drops the 'awaiting-input' state that GuidanceStepOutcomeSchema supports via RecordStepStatusSchema. Unlike the mcp and record branches which correctly preserve 'awaiting-input' through toRecordStatus(), a guidance step awaiting user input will incorrectly report as successful.
if (outcomeType === 'guidance') {
- const status: GuidanceStepOutcome['status'] = ctx.status === 'error' ? 'error' : 'success';
-
- return { type: 'guidance', ...baseFromCtx, status };
+ const status = toRecordStatus(ctx.status);
+
+ return { type: 'guidance', ...baseFromCtx, status } satisfies GuidanceStepOutcome;
}Also found in 1 other location(s)
packages/ai-proxy/src/integrations/zendesk/tools/update-ticket.ts:17
The
statusenum removed'hold'which is a valid Zendesk ticket status. According to Zendesk's API, valid ticket statuses are: new, open, pending, hold, solved, and closed. Removing'hold'prevents users from setting tickets to the 'hold' status, which is a legitimate workflow state used to pause tickets awaiting external responses.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/workflow-executor/src/adapters/run-to-available-step-mapper.ts around lines 63-67:
The `guidance` branch at line 64 only checks for `'error'` status, mapping any other value (including `'awaiting-input'`) to `'success'`. This drops the `'awaiting-input'` state that `GuidanceStepOutcomeSchema` supports via `RecordStepStatusSchema`. Unlike the `mcp` and `record` branches which correctly preserve `'awaiting-input'` through `toRecordStatus()`, a guidance step awaiting user input will incorrectly report as successful.
Evidence trail:
packages/workflow-executor/src/adapters/run-to-available-step-mapper.ts lines 30-35 (toRecordStatus), lines 63-66 (guidance branch), lines 69-75 (mcp/record branches); packages/workflow-executor/src/types/validated/step-outcome.ts lines 11 (RecordStepStatusSchema), lines 57-64 (GuidanceStepOutcomeSchema using RecordStepStatusSchema)
Also found in 1 other location(s):
- packages/ai-proxy/src/integrations/zendesk/tools/update-ticket.ts:17 -- The `status` enum removed `'hold'` which is a valid Zendesk ticket status. According to Zendesk's API, valid ticket statuses are: new, open, pending, hold, solved, and closed. Removing `'hold'` prevents users from setting tickets to the 'hold' status, which is a legitimate workflow state used to pause tickets awaiting external responses.
Summary
workflowHistory.find(s => !s.done && !s.cancelled && !s.context?.error)withworkflowHistory.at(-1)in bothrun-to-available-step-mapper.tsandforest-server-workflow-port.tsTest plan
yarn workspace @forestadmin/workflow-executor testpassesnulldone/cancelled/errorstate🤖 Generated with Claude Code
Note
Use last workflow history entry as current step instead of scanning for first non-done
run-to-available-step-mappernow picks the last entry inworkflowHistoryas the pending step, replacing the previous scan for the first non-done step. This is the core fix described in the PR title.PendingStepExecutiontoAvailableStepExecutionand introducesgetAvailableRuns/getAvailableRunonWorkflowPort, replacinggetPendingStepExecutions/getPendingStepExecutionsForRun.executing/donephases) to mutating step executors (update-record, trigger-action, MCP) to prevent duplicate side effects on retry.updateStepExecution.AvailableStepExecutionreplacesPendingStepExecutionin the public API; callers importing the old type will need to update.📊 Macroscope summarized bafb247. 81 files reviewed, 19 issues evaluated, 6 issues filtered, 5 comments posted
🗂️ Filtered Issues
packages/agent/src/routes/workflow/workflow-executor-proxy.ts — 0 comments posted, 1 evaluated, 1 filtered
options.workflowExecutorUrl.replace(...)without checking fornull. The typeAgentOptionsWithDefaultsisReadonly<Required<AgentOptions>>, butworkflowExecutorUrlhas typestring | null, soRequiredonly removes the optional marker while preserving the| nullunion. If the route is instantiated whenworkflowExecutorUrlisnull, this line throwsTypeError: Cannot read properties of null (reading 'replace'). [ Failed validation ]packages/ai-proxy/src/integrations/zendesk/tools/update-ticket.ts — 0 comments posted, 1 evaluated, 1 filtered
statusenum removed'hold'which is a valid Zendesk ticket status. According to Zendesk's API, valid ticket statuses are: new, open, pending, hold, solved, and closed. Removing'hold'prevents users from setting tickets to the 'hold' status, which is a legitimate workflow state used to pause tickets awaiting external responses. [ Cross-file consolidated ]packages/mcp-server/src/utils/parse-tool-list.ts — 0 comments posted, 1 evaluated, 1 filtered
as ToolName[]cast is unsafe and will accept any string as a valid tool name without validation. If an environment variable contains a typo or invalid tool name (e.g.,"lst"instead of"list"), it will be silently cast toToolName[]and pass type checking, but the invalid tool name will either be silently ignored downstream or cause unexpected behavior. Consider validating each parsed string against the knownToolNamevalues before returning. [ Out of scope (triage) ]packages/workflow-executor/src/adapters/forestadmin-client-activity-log-port.ts — 0 comments posted, 1 evaluated, 1 filtered
errorMessageparameter inmarkFailedis accepted but never sent to the server — it's only used in the catch block for local logging when the update operation fails (line 93). If the intent is to record why the step failed in the audit log, the error message should be included in theupdateActivityLogStatuscall (around line 86), not discarded. Currently, a failed activity log will be marked asstatus: 'failed'with no explanation of what went wrong. [ Out of scope (triage) ]packages/workflow-executor/src/adapters/run-to-available-step-mapper.ts — 1 comment posted, 2 evaluated, 1 filtered
workflowHistoryis empty. If the last step in history hasdone: trueorcancelled: true, the function will incorrectly return it as an available step instead of returning null. The check on line 140 should also verify thatpendingis not already completed, e.g.,if (!pending || pending.done || pending.cancelled) return null;[ Out of scope (triage) ]packages/workflow-executor/src/executors/update-record-step-executor.ts — 1 comment posted, 2 evaluated, 1 filtered
saveStepExecutionfails afterupdateRecordsucceeds at line 228-236, the record is updated in the database but the idempotency phase remains 'executing'. On retry,checkIdempotency()at lines 118-119 sees 'executing' and throwsStepStateError, telling the user to retry manually. However, the update already succeeded, so a manual retry would update the record again, causing duplicate updates. The previous code wrapped this in a try-catch withStepPersistenceErrorthat explicitly stated 'Record update persisted but step state could not be saved', giving context that the update succeeded despite the save failure. [ Cross-file consolidated ]