Skip to content

fix(workflow-executor): use last step from history instead of scanning for first non-done#1581

Closed
Scra3 wants to merge 245 commits into
feat/prd-214-setup-workflow-executor-packagefrom
feat/prd-214-server-step-mapper
Closed

fix(workflow-executor): use last step from history instead of scanning for first non-done#1581
Scra3 wants to merge 245 commits into
feat/prd-214-setup-workflow-executor-packagefrom
feat/prd-214-server-step-mapper

Conversation

@Scra3
Copy link
Copy Markdown
Member

@Scra3 Scra3 commented May 18, 2026

Summary

  • Replace workflowHistory.find(s => !s.done && !s.cancelled && !s.context?.error) with workflowHistory.at(-1) in both run-to-available-step-mapper.ts and forest-server-workflow-port.ts
  • The orchestrator (server) is the source of truth for which step to execute next — the executor should not second-guess it by filtering step states
  • Removes tests that asserted the old filtering logic; updates remaining tests to reflect the new contract

Test plan

  • yarn workspace @forestadmin/workflow-executor test passes
  • Empty history still returns null
  • Last step in history is picked regardless of its done/cancelled/error state

🤖 Generated with Claude Code

Note

Use last workflow history entry as current step instead of scanning for first non-done

  • The run-to-available-step-mapper now picks the last entry in workflowHistory as the pending step, replacing the previous scan for the first non-done step. This is the core fix described in the PR title.
  • Renames PendingStepExecution to AvailableStepExecution and introduces getAvailableRuns/getAvailableRun on WorkflowPort, replacing getPendingStepExecutions/getPendingStepExecutionsForRun.
  • Adds idempotency tracking (executing/done phases) to mutating step executors (update-record, trigger-action, MCP) to prevent duplicate side effects on retry.
  • Adds step execution timeouts (default 5 min), activity log emission per step, and auto-chaining of sequential steps returned by updateStepExecution.
  • Introduces a Kolar integration (KYB/AML screening tools) and extends Zendesk ticket tools with requester, recipient, status, and search filter support.
  • Risk: AvailableStepExecution replaces PendingStepExecution in 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
  • line 24: The constructor accesses options.workflowExecutorUrl.replace(...) without checking for null. The type AgentOptionsWithDefaults is Readonly<Required<AgentOptions>>, but workflowExecutorUrl has type string | null, so Required only removes the optional marker while preserving the | null union. If the route is instantiated when workflowExecutorUrl is null, this line throws TypeError: 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
  • line 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. [ Cross-file consolidated ]
packages/mcp-server/src/utils/parse-tool-list.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 9: The 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 to ToolName[] 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 known ToolName values before returning. [ Out of scope (triage) ]
packages/workflow-executor/src/adapters/forestadmin-client-activity-log-port.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 86: The errorMessage parameter in markFailed is 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 the updateActivityLogStatus call (around line 86), not discarded. Currently, a failed activity log will be marked as status: '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
  • line 139: The function documentation states it "Returns null when the run has no available step (terminal state or all done/cancelled)" but the implementation only returns null when workflowHistory is empty. If the last step in history has done: true or cancelled: true, the function will incorrectly return it as an available step instead of returning null. The check on line 140 should also verify that pending is 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
  • line 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. [ Cross-file consolidated ]

nbouliol and others added 30 commits April 8, 2026 15:01
## @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/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/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
Co-authored-by: alban bertolini <albanb@forestadmin.com>
Co-authored-by: Nicolas Bouliol <nicolas.bouliol@forestadmin.com>
## @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
…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/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))
alban bertolini and others added 25 commits April 30, 2026 15:09
…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
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>
@linear
Copy link
Copy Markdown

linear Bot commented May 18, 2026

PRD-214

@qltysh
Copy link
Copy Markdown

qltysh Bot commented May 18, 2026

22 new issues

Tool Category Rule Count
qlty Structure Function with many returns (count = 4): toStepOutcome 10
qlty Structure Function with high complexity (count = 11): toStepOutcome 8
qlty Structure Function with many parameters (count = 4): forwardRequest 4

@Scra3
Copy link
Copy Markdown
Member Author

Scra3 commented May 18, 2026

Closing duplicate — commit already covered by #1564

@Scra3 Scra3 closed this May 18, 2026
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Low

private async resolveAndLoadAutomatic(target: RelationTarget): Promise<StepExecutionResult> {

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.

Comment on lines +76 to +91
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);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 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),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 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, {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

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

Comment on lines +63 to +67
if (outcomeType === 'guidance') {
const status: GuidanceStepOutcome['status'] = ctx.status === 'error' ? 'error' : 'success';

return { type: 'guidance', ...baseFromCtx, status };
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 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 status enum removed &#39;hold&#39; which is a valid Zendesk ticket status. According to Zendesk's API, valid ticket statuses are: new, open, pending, hold, solved, and closed. Removing &#39;hold&#39; 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.

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.

5 participants