Skip to content

feat(workflow-engine): Introducing a workflow engine to worklows as tools - #111

Merged
gabrypavanello merged 8 commits into
mainfrom
workflow-engine
Jan 23, 2026
Merged

feat(workflow-engine): Introducing a workflow engine to worklows as tools#111
gabrypavanello merged 8 commits into
mainfrom
workflow-engine

Conversation

@gabe4coding

Copy link
Copy Markdown
Contributor
  • Introduced new error classes: WorkflowDefinitionError for invalid workflow definitions and ToolResponseValidationError for failed tool response validations.
  • Updated WorkflowExecutor to support optional response validators for tool calls, improving type safety and error handling.
  • Enhanced the getOrCreateConnection method in ExternalToolClient to handle race conditions for concurrent connection requests.
  • Added a close method to WorkflowExecutor for resource cleanup, ensuring proper management of external tool connections.

These changes improve the robustness of the workflow engine by providing clearer error reporting and validation mechanisms.

@coderabbitai

coderabbitai Bot commented Jan 22, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • v4 workflow engine: multi-step workflows with parallelism, branching, retries, timeouts, and external tool calls; v4 exposed in app versions.
  • UI
    • New Workflow and Advanced Workflow widgets with configurable modals, inputs, run/debug views, and updated styles.
  • Core / Public API
    • Public workflow API, executors/managers (including edge manager), external tool client, and new workflow typings/exports.
  • Tests
    • Extensive tests covering builder, execution, managers, retries, timeouts, branching, and error handling.
  • Documentation
    • Added Workflow Engine docs and examples to READMEs.

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

Adds a new workflow subsystem to core (types, builder, executor, external client, errors, managers), wires an internal tool-caller into ToolContext/server, integrates v4 workflow tools and public typings in the minimal example, and adds UI widgets, styles, and extensive tests for workflow and executor behavior.

Changes

Cohort / File(s) Summary
Core public API
packages/core/src/index.ts, packages/core/src/workflow/index.ts
Exposes the workflow subsystem: factory workflow, executors (WorkflowExecutor), ExternalToolClient, step helpers, error classes, builder types, and executor managers.
Workflow implementation
packages/core/src/workflow/types.ts, .../workflow/workflow-builder.ts, .../workflow/workflow-builder-impl.ts, .../workflow/executor.ts, .../workflow/external-client.ts, .../workflow/steps.ts, .../workflow/errors.ts
New typed workflow system: builder interfaces and impl, runtime executor (retries, timeouts, parallel/branch), external MCP client with connection caching, step helpers, and workflow-specific errors.
Executor managers
packages/core/src/workflow/executor-manager.ts, packages/core/src/workflow/executor-manager-edge.ts
New ExecutorManager (pooling, LRU eviction, TTL cleanup, shutdown) and EdgeExecutorManager (edge-optimized per-invocation pooling, global cleanup hooks).
Server / ToolContext wiring
packages/core/src/server/index.ts, packages/core/src/types/tools.ts
Adds optional _internalToolCaller to ToolContext and wires an internal caller into tool execution to enable internal tool-to-tool invocation.
Example app v4 integration
examples/minimal/src/index.ts
Adds v4 (version "4.0.0"), registers workflow tools (greet_for_workflow, echo_for_workflow, greet_and_echo, process_greeting), and exports AppToolsV4 / AppClientToolsV4.
UI components
examples/minimal/src/ui/WorkflowWidget.tsx, examples/minimal/src/ui/AdvancedWorkflowWidget.tsx
New React widgets to configure/run workflows, handle loading/errors, normalize nested results, and render workflow summaries.
UI styling
examples/minimal/src/ui/styles.css
New workflow-specific CSS (modals, forms, steps, excitement meter); duplicated rule blocks noted.
Tests
packages/core/tests/workflow.test.ts, packages/core/tests/executor-manager.test.ts
Large test suites validating builder rules, step types, execution flows, retries/backoff, error handling, parallelism, branching, timeouts, manager lifecycle, eviction, and cleanup.
Examples / Docs
README.md, packages/core/README.md
Adds Workflow Engine docs and examples (some duplicated content) and updates feature list / API docs to include workflow exports.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Widget as WorkflowWidget
    participant Client
    participant Executor as WorkflowExecutor
    participant ToolCtx as ToolContext
    participant External as ExternalToolClient

    User->>Widget: Click "Run" with inputs
    Widget->>Client: client.tools.callGreet_and_echo(input)
    Client->>Executor: executor.execute(input, toolContext)
    Executor->>Executor: validate input schema, build WorkflowContext
    loop steps
        alt internal tool step
            Executor->>ToolCtx: callTool(toolName, mappedInput)
            ToolCtx->>ToolCtx: _internalToolCaller(toolName, input, ctx)
        else external step
            Executor->>External: callTool(server, toolName, input)
            External->>External: manage connection / invoke MCP
        end
        Executor->>Executor: validate step output, record result
    end
    Executor->>Client: return WorkflowExecutionResult
    Client->>Widget: deliver result
    Widget->>User: render results
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: introducing a workflow engine as tools, which aligns with the extensive workflow-related additions throughout the changeset.
Description check ✅ Passed The description is related to the changeset, covering key improvements including error classes, validation, connection handling, and resource cleanup for the workflow engine.
Docstring Coverage ✅ Passed Docstring coverage is 86.67% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gabrypavanello gabrypavanello self-assigned this Jan 22, 2026
Comment thread packages/core/tests/workflow.test.ts Fixed
Comment thread packages/core/tests/workflow.test.ts Fixed
@claude

claude Bot commented Jan 22, 2026

Copy link
Copy Markdown
Contributor

Code Review: Workflow Engine Feature

Overview

This PR introduces a comprehensive workflow engine that allows composing multi-step tools from existing tools and custom logic. The implementation is well-designed with strong type safety and follows the repository's conventions.

Positive Highlights

1. Excellent Architecture & Design

  • Clean separation of concerns: executor, builder, types, errors, and external client
  • Fluent builder API with progressive type narrowing ensures compile-time safety
  • Supports parallel execution, conditional branching, retries, and error handling
  • Well-documented with JSDoc comments and inline examples

2. Strong Type Safety

  • Proper use of Zod v4 for schema validation
  • No any types in production code (follows repo policy)
  • Type-safe workflow context with accumulated outputs
  • Generic types properly constrained

3. Comprehensive Test Coverage

  • 637 lines of tests covering:
    • Workflow builder validation
    • Sequential and parallel execution
    • Conditional branching
    • Error handling (fail/skip/custom)
    • Retry logic with linear and exponential backoff
    • Timeout handling
    • Tool integration
  • Good use of mocks and fixtures

4. Follows Repository Standards

  • Exports properly flow through src/index.ts
  • Express 5 and Zod 4 used correctly
  • Consistent with existing patterns (e.g., similar to tool builder)
  • Good error classes with proper inheritance

Issues & Concerns

🔴 Critical: Resource Leak in External Tool Client

Location: packages/core/src/workflow/external-client.ts:95-136

Issue: Race condition in connection management could lead to resource leaks.

Problem: If a connection attempt fails, the pending promise is removed but the connection is never added to the cache. However, callers who awaited the same pending promise will get a client that's not tracked, leading to:

  1. No cleanup when closeAll() is called
  2. Potential orphaned connections
  3. The next call will create a new connection instead of reusing

Fix: Ensure error handling propagates correctly and failed connections don't return partial clients.


🟡 Medium: Memory Leak in WorkflowExecutor

Location: packages/core/src/workflow/executor.ts:52-69

Issue: The WorkflowExecutor creates an ExternalToolClient instance but doesn't provide automatic cleanup.

Problem:

  • Workflows created via workflow().build() return a ToolDef, not a WorkflowExecutor
  • The executor is created internally but never exposed
  • Users have no way to call close() to clean up external connections
  • Long-running servers could accumulate zombie MCP client processes

Impact: In production with many workflow executions, this could cause:

  • Process handle exhaustion
  • Memory leaks from unclosed stdio transports
  • Orphaned child processes

Suggested Fix:

  1. Make the executor lifecycle explicit, OR
  2. Implement automatic cleanup with weak references/finalizers, OR
  3. Add a connection pool at the app level that all workflows share

🟡 Medium: Missing Validation for Output Schema

Location: packages/core/src/workflow/executor.ts:144-163

Problem:

  • The output is always taken from the last step only
  • What if the final output should combine results from multiple steps?
  • The lastStep ? ... : ... ternary is redundant since we already checked steps.length === 0

Clarification Needed:

  • Is this intentional design? (last step = final output)
  • Should the docs clarify this behavior?
  • Should there be a way to transform all outputs into the final shape?

🟡 Medium: Unclear Error Handling in closeConnection

Location: packages/core/src/workflow/external-client.ts:229-246

Issues:

  1. Silent error swallowing could hide real problems (e.g., permission errors, process kill failures)
  2. For stdio transports with child processes, there's no actual cleanup code - just a comment
  3. No logging of cleanup failures

Suggestion:

  • At minimum, log cleanup errors at debug/warn level
  • Consider exposing cleanup failures to help diagnose zombie processes

🟢 Minor: Type Assertion Could Be Safer

Location: packages/core/src/workflow/executor.ts:114

Issue: When no validator is provided, the result is blindly cast to TOutput. This bypasses type safety.

Suggestion: Add a runtime check or document that callers are responsible for validation when no validator is provided.


🟢 Minor: Missing Tests for Edge Cases

Missing Coverage:

  1. External tool call failures (network errors, timeout, invalid responses)
  2. Connection pool eviction behavior
  3. Concurrent workflow executions sharing the same external client
  4. Workflow with no output schema but multiple steps (what's returned?)
  5. Error in custom error handler (line 244-250 in executor.ts)
  6. Race condition: multiple parallel steps calling the same external tool

Security Considerations

✅ Good: Input Validation

  • All inputs validated with Zod before execution
  • Tool responses can be validated with optional schemas

⚠️ Concern: Arbitrary External MCP Server Connections

Location: packages/core/src/workflow/external-client.ts:154-166

Issue:

  • Stdio transport executes arbitrary commands from workflow definitions
  • If workflow definitions come from user input, this is a command injection vector
  • No allowlist or validation of permitted servers

Recommendation:

  • Add a configuration option to restrict allowed external servers
  • Document that workflows calling external tools should only use trusted server identifiers
  • Consider adding a server registry/allowlist pattern

Performance Considerations

✅ Good: Connection Pooling

  • Caching MCP client connections (5 min TTL, max 10 connections)
  • LRU eviction when pool is full
  • Prevents creating new connections for every tool call

⚠️ Concern: No Concurrency Limits

  • Parallel steps use Promise.all() with no concurrency control
  • Could overwhelm external services if a workflow has 100+ parallel steps
  • No rate limiting for external tool calls

Suggestion: Consider adding configurable concurrency limits for parallel execution.


Recommendations Summary

Must Fix Before Merge:

  1. Fix external client race condition - Ensure failed connections don't return partial clients
  2. Address executor lifecycle - Provide a way to clean up external connections
  3. Add security controls - Document or restrict external server execution

Should Fix:

  1. Add logging for connection cleanup failures
  2. Clarify output schema behavior in docs
  3. Add tests for external tool error cases and race conditions

Nice to Have:

  1. Concurrency limits for parallel steps
  2. Migration guide and performance docs

Conclusion

This is a high-quality feature with excellent design and implementation. The workflow engine is well-architected, type-safe, and follows repository conventions. The test coverage is comprehensive.

However, there are critical resource management issues that could cause memory leaks and orphaned processes in production. These must be addressed before merge.

Once the resource lifecycle issues are resolved, this will be a valuable addition to the framework.

Recommendation: Request changes for resource management fixes, then approve.


🤖 Generated with Claude Code

Add a comprehensive workflow engine that allows composing multiple tools
into reusable workflows with advanced control flow capabilities.

Features:
- Sequential, parallel, and conditional step execution
- Tool steps (call other tools), custom steps (inline logic), and external steps (call MCP servers)
- Accumulated context passing data between steps
- Configurable error handling (fail, skip, or custom handler)
- Retry logic with exponential/linear backoff
- Step timeouts and input mapping
- Full TypeScript type safety with Zod validation
- External tool client with connection caching and LRU eviction

API:
- Fluent builder: workflow(name).describe().input().output().step().build()
- Step helpers: toolStep(), customStep(), externalStep()
- Compiles to standard ToolDef for seamless createApp integration

Implementation:
- Core types and interfaces in workflow/types.ts
- Step helpers in workflow/steps.ts
- Fluent builder in workflow/builder.ts and workflow-builder-impl.ts
- Runtime executor in workflow/executor.ts with retry and error handling
- External MCP client in workflow/external-client.ts with caching
- Custom error classes in workflow/errors.ts
- Comprehensive test suite with 32 tests

Exports all workflow APIs from core package index.
Enable workflows to call other tools in the same app by injecting an
internal tool caller into the ToolContext.

Implementation:
- Add _internalToolCaller property to ToolContext (marked @internal)
- Create tool caller function during tool registration with closure access to all tools
- Inject caller into every tool's context for use by workflows
- Workflow executor uses _internalToolCaller to invoke tools directly
- Validates input via Zod schemas and preserves context (auth, locale, state)

Benefits:
- Zero configuration - works automatically for all workflows
- Type-safe with proper validation
- Context preservation through nested calls
- No global registry or circular dependencies
- Enables toolStep() to work seamlessly in workflows

This allows workflows to compose existing tools without requiring
external MCP server setup for internal tool calls.
Add comprehensive workflow examples to the minimal app demonstrating
the workflow engine capabilities with rich interactive UI widgets.

New Workflows (v4 API):
- greet_and_echo: Sequential workflow with tool calls and custom logic
  * Calls greet_for_workflow tool
  * Adds excitement transformation
  * Calls echo_for_workflow with uppercase
  * Combines results with timestamp

- process_greeting: Advanced workflow with parallel and conditional execution
  * Parallel processing of multiple names
  * Conditional branching (formal vs casual format)
  * Demonstrates complex control flow

UI Components:
- WorkflowWidget: Interactive UI for greet_and_echo workflow
  * Name input and excitement level slider (1-10)
  * Step-by-step result display
  * Excitement meter visualization
  * Beautiful gradient design

- AdvancedWorkflowWidget: Interactive UI for process_greeting workflow
  * Dynamic name list management (add/remove)
  * Format selection (casual/formal radio buttons)
  * Greetings list display
  * Features checklist visualization

Styling:
- Workflow-specific gradients and glassmorphism effects
- Step result cards with backdrop blur
- Range slider with custom styling
- Form controls (radio, dynamic lists)
- Full dark mode support

Both workflows demonstrate proper output handling, nested tool calls,
and integration with the UI system via defineReactUI().
Enable workflows to call external MCP servers via HTTP in addition to
stdio transport for greater flexibility and remote server support.

Implementation:
- Import StreamableHTTPClientTransport from MCP SDK
- Auto-detect transport based on server identifier format
  * HTTP/HTTPS URLs → StreamableHTTPClientTransport
  * Command names → StdioClientTransport (stdio)
- Pass URL object to StreamableHTTPClientTransport constructor

Transport Selection:
- stdio: "weather-server" or "mcp://weather-server"
- HTTP: "http://localhost:3000/mcp" or "https://api.example.com/mcp"

Documentation:
- Updated ExternalToolCaller type documentation with examples
- Updated ExternalStepConfig with transport details
- Updated externalStep() JSDoc with stdio and HTTP examples

Benefits:
- Connect to remote MCP servers over HTTP
- Unified API for both local and remote tool calls
- Automatic protocol detection
- Supports all StreamableHTTP features (sessions, auth, reconnection)

This allows workflows to seamlessly integrate with both local and
remote MCP servers without changing the workflow definition API.
- Introduced new error classes: WorkflowDefinitionError for invalid workflow definitions and ToolResponseValidationError for failed tool response validations.
- Updated WorkflowExecutor to support optional response validators for tool calls, improving type safety and error handling.
- Enhanced the getOrCreateConnection method in ExternalToolClient to handle race conditions for concurrent connection requests.
- Added a close method to WorkflowExecutor for resource cleanup, ensuring proper management of external tool connections.

These changes improve the robustness of the workflow engine by providing clearer error reporting and validation mechanisms.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@packages/core/src/server/index.ts`:
- Around line 748-771: internalToolCaller currently calls targetToolDef.handler
directly, bypassing middleware, plugin hooks, event emission, and output
validation; instead, route internal calls through the same tool execution
pipeline used for external calls by invoking the shared tool-invocation helper
(or extract one if it doesn't exist) so both registered tool execution and
internalToolCaller use it. Modify internalToolCaller to: 1) perform input
parsing as now but then call the central execution function (e.g.,
executeTool/runTool/invokeTool — create this helper if missing) which runs
middleware, plugin hooks, emits events, executes the handler, and runs output
validation; 2) keep passing ToolContext (including state and
_internalToolCaller) so internal calls retain context; and 3) remove direct
calls to targetToolDef.handler so auth/rate-limit/telemetry and validation
remain consistent.

In `@packages/core/src/workflow/executor.ts`:
- Around line 402-434: The parallel and branch executors bypass per-step
policies by directly calling executeStep; change them to route each child
through a common helper that enforces StepConfig (retry/timeout/onError) such as
a new private method (e.g., executeStepWithPolicy or applyStepConfigAndExecute)
and use that in executeParallelStep and executeBranchStep instead of
executeStep; the helper should read the child step's config, wrap the
executeStep(childStep, context) call with the retry/timeout/onError logic
already used for top-level steps, and return the child result (or propagate
handled errors) so nested steps honor their own policies.
- Around line 197-327: executeNamedStep currently records retries as a
zero-based index and timeouts create StepTimeoutError with a literal "step"
name; update the retry metadata and timeout error naming by (1) changing the
onRetry handler usage so it sets retries = attempt + 1 (i.e., convert the
zero-based attempt index to a 1-based attempt count) in
executeNamedStep/executeStepWithRetry, and (2) propagate the step name into
timeout errors by adding an optional stepName parameter to executeStepWithRetry
and executeStepWithTimeout (or otherwise pass the NamedStep.name from
executeNamedStep into executeStepWithRetry), then use that stepName when
constructing the StepTimeoutError in executeStepWithTimeout instead of the
hardcoded "step".

In `@packages/core/src/workflow/workflow-builder-impl.ts`:
- Around line 189-217: The ToolDef.output cast can remain as-is, but you must
ensure the WorkflowExecutor's resources are cleaned up: either call
executor.close() at the end of the tool handler's execution (after
WorkflowExecutor.run/execute completes and after any async work), or expose a
disposal API on the returned tool definition (e.g., add a close()/dispose()
function property on the ToolDef that calls executor.close()) so external code
can explicitly free resources; update the code that constructs the
WorkflowExecutor (symbol: WorkflowExecutor, local: executor) and the tool
creation block (symbol: ToolDef) to implement one of these two cleanup
approaches.

In `@packages/core/tests/workflow.test.ts`:
- Around line 456-491: In the "should retry with exponential backoff" test,
replace real timers with Vitest fake timers: call vi.useFakeTimers() before
invoking wf.handler and vi.useRealTimers() in a finally block; start the handler
without awaiting, then drive retries deterministically by calling
vi.advanceTimersByTimeAsync() for the expected delays (10ms then 20ms for
exponential backoff) before awaiting the handler result; keep assertions on
attempts, delays, and ordering but compute delays from controlled timer
advancement (or remove Date.now usage and assert the number/order of advances)
so the test is deterministic and not environment-dependent (targets: the test
name string, workflow builder (workflow(...).step(..., { retry: { maxAttempts:
3, delay: 10, backoff: "exponential" } })), customStep, and wf.handler).
🧹 Nitpick comments (6)
packages/core/src/workflow/external-client.ts (2)

229-246: Consider logging transport close failures for debugging.

The nested catch blocks silently swallow errors. While the best-effort cleanup approach is reasonable, completely silent failures may make debugging difficult in production.

🔧 Optional: Add debug logging for close failures
 private async closeConnection(connection: CachedConnection): Promise<void> {
   try {
     await connection.client.close();
   } catch {
     // If Client.close() fails, attempt to close the transport directly
     try {
       await connection.transport.close();
-    } catch {
+    } catch (transportError) {
       // Transport close also failed - for stdio transport, try to kill the process
       if (connection.transport instanceof StdioClientTransport) {
         // StdioClientTransport has internal process management
         // The close() call above should handle it, but if it doesn't,
         // we've done our best effort
       }
-      // Swallow the error - we've attempted cleanup
+      // Best-effort cleanup - log for debugging if needed
+      // console.debug("Failed to close transport:", transportError);
     }
   }
 }

251-256: closeAll should handle individual connection failures gracefully.

If one connection fails to close, the loop continues but errors are lost. Consider using Promise.allSettled for parallel cleanup or accumulating errors.

♻️ Suggested improvement for resilient cleanup
 async closeAll(): Promise<void> {
-  for (const connection of this.connections.values()) {
-    await this.closeConnection(connection);
-  }
+  const closePromises = Array.from(this.connections.values()).map((connection) =>
+    this.closeConnection(connection)
+  );
+  await Promise.allSettled(closePromises);
   this.connections.clear();
 }
examples/minimal/src/index.ts (1)

244-267: Type assertions on workflow context could be improved.

The repeated ctx.input as { name: string } and ctx.outputs.greet as { message: string } assertions work but reduce type safety. This is acceptable for example code demonstrating the workflow API, but production workflows would benefit from stronger typing.

examples/minimal/src/ui/WorkflowWidget.tsx (2)

174-179: Add radix to parseInt for clarity.

While parseInt defaults to base 10 for numeric strings, explicitly specifying the radix improves code clarity and avoids potential edge cases.

🔧 Minor fix
                onChange={(e) =>
                  setWorkflowInput({
                    ...workflowInput,
-                   excitement: parseInt(e.target.value),
+                   excitement: parseInt(e.target.value, 10),
                  })
                }

148-202: Modal accessibility could be improved.

The modal implementation works but could benefit from focus trapping and ARIA attributes for better accessibility.

♿ Optional accessibility improvements
       {isModalOpen && (
-        <div className="modal-overlay" onClick={() => setIsModalOpen(false)}>
-          <div className="modal" onClick={(e) => e.stopPropagation()}>
+        <div className="modal-overlay" onClick={() => setIsModalOpen(false)} role="dialog" aria-modal="true" aria-labelledby="modal-title">
+          <div className="modal" onClick={(e) => e.stopPropagation()}>
-            <h2>Configure Workflow</h2>
+            <h2 id="modal-title">Configure Workflow</h2>
packages/core/src/workflow/workflow-builder-impl.ts (1)

106-171: Preserve input/output type generics through step/parallel/branch methods.

The implementations of step(), parallel(), branch(), and ui() return hard-coded WorkflowBuilderWithSteps<TName, z.ZodType, z.ZodType> (lines 110, 124, 131, 145, 155, 170, 173), which drops the concrete TInput and TOutput generics set by prior input()/output() calls. The interfaces promise to preserve these types (WorkflowBuilderWithOutput.step() returns WorkflowBuilderWithSteps<TName, TInput, TOutput>; parallel() and branch() return this), but the implementation doesn't deliver.

Consider making the class generic over TInput and TOutput to maintain type safety through the entire fluent API chain, ensuring build() retains strong typing.

Comment thread packages/core/src/server/index.ts
Comment thread packages/core/src/workflow/executor.ts
Comment thread packages/core/src/workflow/executor.ts
Comment thread packages/core/src/workflow/workflow-builder-impl.ts
Comment thread packages/core/tests/workflow.test.ts
@claude

claude Bot commented Jan 22, 2026

Copy link
Copy Markdown
Contributor

Code Review: Workflow Engine Feature

Summary

This PR introduces a comprehensive workflow engine that allows composing multi-step tools from existing tools and custom logic. The implementation is well-structured with strong type safety, good error handling, and thorough test coverage (637 lines of tests).


✅ Strengths

Architecture & Design

  • Excellent type safety: Fluent builder pattern with progressive type refinement ensures workflows are correctly configured at compile time
  • Clean separation of concerns: Well-organized modules (types, errors, executor, external-client, builder)
  • Composable design: Supports sequential, parallel, conditional, and external tool steps
  • Resource management: Proper lifecycle management with close() method and connection pooling

Error Handling

  • Comprehensive error hierarchy: Specific error classes (WorkflowExecutionError, StepTimeoutError, ToolResponseValidationError, etc.) with structured details
  • Flexible error strategies: Step-level error handling with "fail", "skip", or custom recovery functions
  • Validation: Input/output schema validation with clear error messages

Features

  • Retry logic: Configurable retry with linear/exponential backoff and max delay caps
  • External MCP integration: Supports both stdio and HTTP transports with connection caching
  • Timeout support: Per-step timeout configuration
  • Context accumulation: Steps can access outputs from previous steps through workflow context

🔍 Issues & Recommendations

1. CRITICAL: Race Condition in Connection Management (packages/core/src/workflow/external-client.ts:95-136)

The getOrCreateConnection method has a race condition window:

// Check if we have a cached connection
const cached = this.connections.get(server);
if (cached) {
  cached.lastUsed = Date.now();  // ✅ Good
  return cached.client;
}

// Check if a connection is already being created
const pending = this.pendingConnections.get(server);
if (pending) {
  const { client } = await pending;  // ⚠️ Returns client but doesn't update cache timestamp
  return client;
}

Issue: When waiting for a pending connection, the code doesn't update the lastUsed timestamp in the cache. This could cause premature eviction of actively-used connections.

Fix: After awaiting the pending connection, update the timestamp:

if (pending) {
  const { client } = await pending;
  // Update timestamp for concurrent access
  const cached = this.connections.get(server);
  if (cached) {
    cached.lastUsed = Date.now();
  }
  return client;
}

2. Memory Leak Risk: No Cleanup on Workflow Completion (packages/core/src/workflow/workflow-builder-impl.ts:218-228)

The workflow handler creates a WorkflowExecutor but never cleans it up:

handler: async (input: z.infer<TInput>, context: ToolContext) => {
  const result = await executor.execute(input, context);  // ⚠️ executor.close() never called
  return result.output;
}

Issue: Each workflow execution may open external MCP connections, but there's no cleanup mechanism. This will accumulate connections over time.

Recommendation:

  • Option A (Preferred): Use a shared executor with connection pooling (current cache helps but doesn't solve the lifecycle issue)
  • Option B: Add try-finally to ensure cleanup:
handler: async (input, context) => {
  try {
    const result = await executor.execute(input, context);
    return result.output;
  } finally {
    await executor.close();
  }
}

However, Option B would close connections after every workflow invocation, defeating the cache. Consider implementing a TTL-based cleanup or shared executor pattern.


3. Type Safety Gap: Validator Discrimination (packages/core/src/workflow/executor.ts:176-192)

if (typeof validator === 'function') {
  return validator(result);
} else if (typeof validator === 'object' && 'parse' in validator) {
  return validator.parse(result);
}

Issue: This runtime type check is fragile. All functions are objects, and the order matters. A Zod schema could theoretically be callable.

Better approach: Use type guards or check for Zod-specific properties first:

// Check for Zod schema first (more specific)
if (validator && typeof validator === 'object' && 'parse' in validator && typeof validator.parse === 'function') {
  return validator.parse(result);
} else if (typeof validator === 'function') {
  return validator(result);
}
throw new Error('Invalid validator: must be a Zod schema or validation function');

4. Incorrect Error Message (packages/core/src/workflow/executor.ts:314)

reject(new StepTimeoutError('step', timeout));

Issue: Hardcoded string "step" instead of actual step name. This makes debugging timeouts nearly impossible in multi-step workflows.

Fix: Pass the actual step name (requires refactoring to pass step name through the call chain):

private async executeStepWithTimeout(
  step: Step,
  stepName: string,  // Add parameter
  context: WorkflowContext,
  timeout: number
): Promise<unknown> {
  // ...
  reject(new StepTimeoutError(stepName, timeout));
}

5. Inconsistent Error Handling in Close Methods (packages/core/src/workflow/external-client.ts:229-245)

try {
  await connection.client.close();
} catch {
  try {
    await connection.transport.close();
  } catch {
    // Swallow the error
  }
}

Issues:

  • Silent failures make debugging difficult
  • No logging of cleanup failures
  • Resource leak detection is impossible

Recommendation: At minimum, use the debug logger:

import { debugLogger } from '../debug/logger';

try {
  await connection.client.close();
} catch (error) {
  debugLogger.log('workflow', 'error', 'Failed to close client', { error, server });
  try {
    await connection.transport.close();
  } catch (transportError) {
    debugLogger.log('workflow', 'error', 'Failed to close transport', { 
      error: transportError, 
      server 
    });
  }
}

6. Potential Type Confusion (packages/core/src/types/tools.ts - new additions)

The PR adds _internalToolCaller to ToolContext. Review the type definition to ensure:

  • It's properly marked as optional to maintain backward compatibility
  • The type signature matches the implementation in server/index.ts:750-764

7. Test Coverage Gaps

While test coverage is good (637 lines), I noticed potential gaps:

Missing test scenarios:

  • Concurrent workflows sharing external connections (race conditions)
  • Connection eviction under load
  • Workflow cleanup/resource disposal
  • Timeout during pending connection creation
  • Error handler throwing errors (partially covered but edge cases missing)
  • Exponential backoff boundary conditions (max delay capping)
  • Parallel step failures with different error strategies
  • Branch with async condition function

🔧 Code Quality Issues

Minor Issues

  1. Redundant type checking (packages/core/src/workflow/executor.ts:440):
const config = step.type === 'parallel' || step.type === 'branch' ? step.config : step.config;

This is a no-op - both branches return step.config. The intent seems to be handling the different config types, but this doesn't achieve that.

  1. Magic numbers (packages/core/src/workflow/external-client.ts:34-35):
private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes
private readonly MAX_CONNECTIONS = 10;

Consider making these configurable through constructor options.

  1. Unclear fallback behavior (packages/core/src/workflow/external-client.ts:64-78):
    The fallback from structuredContent to parsing text content is undocumented. This could cause silent data loss if the contract changes.

📝 Documentation

Good

  • Comprehensive JSDoc comments
  • Excellent examples in workflow/index.ts
  • Clear type definitions

Improvements Needed

  • Document the lifecycle of WorkflowExecutor (when to create/close)
  • Clarify external connection pooling behavior
  • Document the _internalToolCaller addition to ToolContext
  • Add migration guide for existing tool definitions

🎯 Performance Considerations

  1. Connection pooling is good, but consider:

    • Configurable pool size
    • Health checks for cached connections
    • Circuit breaker pattern for failing external servers
  2. Parallel execution: Uses Promise.all which fails fast. Consider Promise.allSettled with proper error aggregation for better resilience.


🔐 Security Concerns

  1. Command injection risk (packages/core/src/workflow/external-client.ts:162):
transport = new StdioClientTransport({
  command: serverName,  // ⚠️ User input executed as command
  args: [],
});

While this is expected behavior, ensure workflows are not created from untrusted input. Document this security consideration.

  1. No rate limiting on external tool calls - could be used for DoS attacks against external services.

✅ Final Verdict

Recommendation: Approve with required changes

This is a well-designed feature with excellent type safety and comprehensive testing. However, the critical issues (#1 and #2) should be addressed before merging:

  1. Fix the race condition in connection timestamp updates
  2. Implement proper executor lifecycle management

The other issues are important but not blocking for an initial merge. Consider creating follow-up issues for:

  • Enhanced test coverage (especially concurrency scenarios)
  • Configurable connection pool settings
  • Better error logging in cleanup paths
  • Documentation improvements

Great work overall! This is a valuable addition to the framework. 🎉


Checklist Summary

  • ✅ Code quality: Excellent architecture and type safety
  • ⚠️ Potential bugs: 2 critical issues requiring fixes
  • ✅ Performance: Good design with connection pooling
  • ⚠️ Security: Document command injection risk
  • ✅ Test coverage: Comprehensive with identified gaps
  • ✅ Best practices: Follows repo conventions (Zod 4, Express 5, strict TypeScript)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@examples/minimal/src/ui/styles.css`:
- Around line 446-495: Add a visible keyboard focus state for the custom range
control by styling the focused thumb and track: update rules for
input[type="range"] to include a focus selector (e.g., input[type="range"]:focus
and input[type="range"]:focus-visible) and add focused pseudo-elements for the
webkit and moz thumbs (input[type="range"]::-webkit-slider-thumb:focus /
:focus-visible and input[type="range"]::-moz-range-thumb:focus / :focus-visible)
and focused track (input[type="range"]::-webkit-slider-runnable-track and
::-moz-range-track when the input is focused) so keyboard users see a clear
ring/outline (contrasting color and sufficient thickness) and ensure the dark
theme (.dark) has a matching focus color; apply these changes near the existing
input[type="range"]::-webkit-slider-thumb and ::-moz-range-thumb blocks.
- Around line 379-408: The .remove-btn and .add-btn classes currently only
define hover styles and lack keyboard focus indicators; update the stylesheet to
add :focus and preferably :focus-visible rules for both .remove-btn and .add-btn
(matching the existing hover visual language) to provide a clear, visible
outline or box-shadow and ensure outline-offset/contrast for keyboard users, and
include :focus { outline: none } only if replaced by an accessible custom focus
style so the focus state remains visible; modify the rules near the .remove-btn
and .add-btn blocks to include these focus selectors.

In `@packages/core/src/workflow/errors.ts`:
- Around line 89-97: The constructor of ToolResponseValidationError currently
spreads details after toolName which allows details.toolName to override the
canonical toolName; change the object passed to super so toolName wins (e.g.,
spread details first then toolName: super(message,
"TOOL_RESPONSE_VALIDATION_ERROR", { ...details, toolName }) or use
Object.assign({}, details, { toolName })) in the ToolResponseValidationError
constructor to ensure the public readonly toolName always takes precedence.
- Around line 54-64: The constructor for ExternalToolError builds metadata with
"{ server, toolName, ...details }" which allows caller-provided details to
override server/toolName; change the merge order so caller details are applied
first and then server/toolName override them (e.g., merge details before
server/toolName) when calling super in ExternalToolError's constructor to ensure
the public readonly server and toolName remain authoritative.

In `@packages/core/src/workflow/external-client.ts`:
- Around line 45-56: In callTool, avoid unsafely casting the unknown input to
Record<string,unknown> before passing to client.callTool; instead validate input
is a plain object (e.g., object literal, not array/null) and only set the
arguments field when that check passes, otherwise call client.callTool without
the arguments property (or throw a validation error). Update the logic around
client.callTool in the callTool method (and any helper used like
getOrCreateConnection) to either perform the object-shape check and pass { name:
toolName, arguments: input as Record<string,unknown> } only when valid, or omit
the arguments key entirely for non-object inputs. Ensure the change preserves
Promise<unknown> return behavior and covers primitives/arrays/null safely.

In `@packages/core/tests/workflow.test.ts`:
- Around line 172-176: Add a Biome ignore comment to each branch configuration
object that uses the then property (e.g., the .branch("branch1", { when: ...,
then: [...], else: [...] }) calls) to suppress the noThenProperty lint rule;
place a line like "// biome-ignore noThenProperty" immediately above each
offending branch config (the ones around the .branch calls containing then/else
arrays) so the tests keep the API intact without lint errors.
♻️ Duplicate comments (1)
packages/core/src/server/index.ts (1)

748-771: Internal tool calls bypass the standard execution pipeline.

Line 748–764 directly invokes targetToolDef.handler, skipping middleware, plugin hooks, event emission, and output validation. This can diverge from normal tool execution and bypass auth/rate-limit/telemetry.

Please route internal calls through the same centralized execution path used by normal tool invocations (extract/reuse the existing helper) so internal and external behavior stays consistent.

🧹 Nitpick comments (1)
examples/minimal/src/ui/styles.css (1)

249-259: Consider adding explicit text color and dark mode variant.

.workflow-result sets only a background gradient without an explicit text color. If inherited color changes, readability could be affected. Also, there's no .dark .workflow-result variant defined.

💡 Suggested improvement
 .workflow-result {
   background: linear-gradient(135deg, `#f093fb` 0%, `#f5576c` 100%);
+  color: white;
 }
+
+.dark .workflow-result {
+  background: linear-gradient(135deg, `#d063db` 0%, `#d5374c` 100%);
+}

Comment thread examples/minimal/src/ui/styles.css
Comment thread examples/minimal/src/ui/styles.css
Comment thread packages/core/src/workflow/errors.ts
Comment thread packages/core/src/workflow/errors.ts
Comment thread packages/core/src/workflow/external-client.ts
Comment thread packages/core/tests/workflow.test.ts
- Introduced ExecutorManager and EdgeExecutorManager for improved lifecycle management of workflow executors, optimizing resource usage in both traditional and edge environments.
- Added new error classes: WorkflowDefinitionError and ToolResponseValidationError for better error reporting during workflow execution.
- Updated WorkflowExecutor to support enhanced validation and error handling, including improved retry logic and step configuration management.
- Enhanced ExternalToolClient with configurable connection settings and improved error handling during connection closure.

These changes significantly improve the robustness and flexibility of the workflow engine, enabling better performance and error management across different execution environments.
Comment thread packages/core/tests/executor-manager.test.ts Fixed
@claude

claude Bot commented Jan 23, 2026

Copy link
Copy Markdown
Contributor

Pull Request Review: Workflow Engine Implementation

Summary

This PR introduces a comprehensive workflow engine for composing multi-step tool workflows in @mcp-apps-kit/core. The implementation is well-architected with strong type safety, comprehensive error handling, and production-ready lifecycle management.

Code Quality: ⭐⭐⭐⭐ (4/5)

Strengths

1. Architecture & Design

  • Clean separation of concerns across modules (types, executor, builder, manager)
  • Fluent builder API with progressive type narrowing enforces correct usage at compile-time
  • Excellent use of TypeScript generics and Zod for end-to-end type safety
  • Smart environment detection for Node.js vs Edge/Serverless deployments

2. Error Handling

  • Comprehensive custom error classes with structured metadata (WorkflowDefinitionError, ToolResponseValidationError, etc.)
  • Proper error wrapping to prevent double-wrapping (lines 419-422 in executor.ts, 155-158 in external-client.ts)
  • Flexible error recovery strategies: fail, skip, or custom handlers

3. Production Features

  • Connection caching with LRU eviction in ExternalToolClient
  • Executor pooling and lifecycle management via ExecutorManager
  • Reference counting prevents premature cleanup of active executors
  • Automatic cleanup with configurable TTL and intervals
  • Edge-optimized variant (EdgeExecutorManager) with smaller pool size

4. Documentation

  • Excellent inline JSDoc with usage examples
  • Comprehensive module-level documentation in index.ts (lines 1-176)
  • Clear separation of public API vs internal implementations

Issues Found

🔴 Critical

1. Race Condition in Connection Management (packages/core/src/workflow/external-client.ts:186-214)

The getOrCreateConnection method has a race condition fix but introduces a new issue:

// Line 189: Eviction happens BEFORE checking pending connections
if (this.connections.size >= this.MAX_CONNECTIONS) {
  await this.evictOldConnections();
}

// Line 194: Create new pending connection
pending = this.createConnectionWithTransport(server);
this.pendingConnections.set(server, pending);

Problem: If two concurrent requests for different servers arrive when the cache is full:

  1. Request A checks cache (full), evicts LRU, creates pending for server A
  2. Request B checks cache (still full due to pending), evicts again unnecessarily

Fix: Check pendingConnections.size in the eviction condition:

if (this.connections.size + this.pendingConnections.size >= this.MAX_CONNECTIONS) {
  await this.evictOldConnections();
}

2. Memory Leak in Timeout Implementation (packages/core/src/workflow/executor.ts:336-353)

The timeout cleanup has a subtle bug:

let timer: ReturnType<typeof setTimeout> | undefined;

const timeoutPromise = new Promise<never>((_, reject) => {
  timer = setTimeout(() => {
    reject(new StepTimeoutError(stepName, timeout));
  }, timeout);
});

try {
  const result = await Promise.race([this.executeStep(step, context), timeoutPromise]);
  return result;
} finally {
  if (timer !== undefined) {
    clearTimeout(timer);
  }
}

Problem: If the step completes successfully before timeout, the timer is cleared. However, if the step throws an error that's not a timeout, the Promise.race rejects but the timeout timer continues running until it fires, keeping the promise alive.

Fix: Use AbortController or ensure the timer is always cleared:

try {
  const result = await Promise.race([...]);
  if (timer !== undefined) clearTimeout(timer);
  return result;
} catch (error) {
  if (timer !== undefined) clearTimeout(timer);
  throw error;
}

Or move clearTimeout before the return/throw.

🟡 High Priority

3. Missing Validation in Workflow Builder (packages/core/src/workflow/workflow-builder-impl.ts)

The builder validates duplicate step names and requires at least one step, but doesn't validate:

  • Circular references in step dependencies (step A uses output from step B which uses output from step A)
  • Steps that reference undefined output keys from previous steps
  • Branch conditions that might access unavailable context

Recommendation: Add static analysis in build() to detect common mistakes at build time rather than runtime.

4. Potential Stack Overflow in Nested Workflows (packages/core/src/workflow/executor.ts:358-372)

The executeStep method recursively handles parallel and branch steps. For deeply nested workflows (e.g., branches within parallel steps within branches), this could cause stack overflow.

Recommendation: Consider iterative execution with an explicit stack for very deep nesting, or document maximum nesting depth.

5. Input Validation Inconsistency (packages/core/src/workflow/external-client.ts:22-30)

External tool input is validated to be a plain object, but internal tool steps don't have this validation:

private async executeToolStep(step: ToolStep, context: WorkflowContext): Promise<unknown> {
  const input = step.config?.mapInput ? step.config.mapInput(context) : context.input;
  return context.callTool(step.toolName, input); // No validation that input is an object
}

Recommendation: Apply consistent validation or document why external tools need stricter validation.

🟢 Medium Priority

6. Test Coverage Gaps

Based on the test file (workflow.test.ts), missing test coverage for:

  • External tool error scenarios (connection failures, malformed responses)
  • Executor manager eviction under load (concurrent workflows filling cache)
  • Edge executor manager behavior
  • Timeout edge cases (timeout during retry, timeout in parallel steps)
  • Output schema validation failures
  • Structured content vs text content fallback in external client

Recommendation: Add integration tests for ExternalToolClient and ExecutorManager.

7. Performance Consideration: Sequential Branch Execution (packages/core/src/workflow/executor.ts:458-466)

Branch steps execute sequentially:

for (const childStep of stepsToExecute) {
  const result = await this.executeStep(childStep, context);
  results.push(result);
}

While this maintains order, it doesn't allow parallel execution within a branch. Consider allowing developers to mark branch steps as parallelizable.

8. Cleanup Interval Memory in Edge Environments (packages/core/src/workflow/external-client.ts:79-89)

The code attempts to prevent timers in edge environments:

if (typeof setInterval !== "undefined" && typeof process !== "undefined") {
  this.cleanupInterval = setInterval(...);
}

Issue: This check isn't reliable. Edge runtimes like Deno, Bun, and Cloudflare Workers have setInterval and process globals.

Recommendation: Use explicit environment detection via process.env.EDGE_RUNTIME or configuration parameter.

Security Considerations: ✅ Good

Strengths

  • Input validation using Zod schemas prevents injection attacks
  • External tool input restricted to plain objects (no prototype pollution)
  • No use of eval() or Function() constructors
  • Proper error sanitization (no raw error objects exposed)

Recommendations

  1. Rate Limiting: Consider adding rate limiting for external tool calls to prevent abuse
  2. Timeout Defaults: External tool calls have no default timeout, could lead to resource exhaustion
  3. Server Validation: The externalStep accepts arbitrary server URLs - consider allowlist configuration

Performance Considerations: ✅ Good

Strengths

  • Connection pooling with LRU eviction
  • Executor reuse via manager singleton
  • Parallel step execution
  • Lazy executor creation

Recommendations

  1. Metrics: Add performance metrics (step duration, retry counts) for monitoring
  2. Batch Operations: For workflows calling the same external tool multiple times, consider batching
  3. Memory Limits: Consider max workflow size limits (number of steps, context size)

Test Coverage: ⚠️ Needs Improvement

The test file has 32 tests covering builder validation, execution, error handling, and retry logic. However:

  • Missing: External client tests, executor manager tests (basic test in executor-manager.test.ts but needs more coverage)
  • Missing: Edge case tests for timeout cleanup, connection failures, malformed responses
  • Missing: Load tests for executor pool eviction
  • Missing: Integration tests for full workflow lifecycle

Recommendation: Achieve 80%+ coverage before merging, especially for ExecutorManager and ExternalToolClient.

API Design: ⭐⭐⭐⭐⭐ (5/5)

The fluent builder API is excellent:

workflow("process_order")
  .describe("Process order")
  .input({ orderId: z.string() })
  .output({ success: z.boolean() })
  .step("validate", toolStep("validate_order"))
  .step("payment", toolStep("process_payment"), { retry: { maxAttempts: 3 } })
  .build()
  • Progressive type narrowing prevents invalid states at compile-time
  • Helper functions (toolStep, customStep, externalStep) improve readability
  • Consistent configuration via StepConfig interface

Code Style: ✅ Excellent

  • Consistent naming conventions
  • Proper TypeScript strict mode compliance
  • Clear module boundaries
  • Good use of JSDoc
  • No any types (follows repo policy)

Breaking Changes: ✅ None

All changes are additive. Existing APIs remain unchanged.

Overall Recommendation: ⚠️ APPROVE WITH CHANGES

This is high-quality work with strong architecture and design. However, the critical issues should be addressed before merging:

Required Changes:

  1. Fix race condition in ExternalToolClient connection management
  2. Fix timeout memory leak in WorkflowExecutor
  3. Add missing test coverage for ExternalToolClient and ExecutorManager
  4. Address edge environment timer detection

Suggested Changes:

  1. Add workflow validation for common mistakes (circular deps, undefined outputs)
  2. Add default timeouts for external tool calls
  3. Document maximum workflow nesting depth
  4. Consider adding performance metrics/monitoring hooks

Great work overall! The workflow engine is a valuable addition to the framework. Once the critical issues are resolved, this will be production-ready.


Reviewed by: Claude Code (Sonnet 4.5)
Review Date: 2026-01-23

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@packages/core/src/server/index.ts`:
- Around line 395-399: The stop function currently awaits
ExecutorManager.getInstance().shutdown() and if that throws the HTTP server
isn't closed; wrap the executor shutdown in try/finally so the server close
logic always runs (call the server.close()/closeServerPromise or existing
shutdown helper in the finally block), then if shutdown threw rethrow the error
after closing the server; update the stop implementation that references
ExecutorManager.getInstance().shutdown() to ensure server closure happens in
finally and errors are propagated.

In `@packages/core/src/workflow/executor-manager.ts`:
- Around line 145-148: Current eviction unconditionally calls evictLRU() when
this.executors.size >= this.config.maxExecutors which can close an active
executor; change the logic to only consider idle executors for eviction: inspect
the executor entries in this.executors, filter for idle/not-in-flight instances
(use the executor state flag/property used in Executor class or map values),
pick the least-recently-used idle executor and evict it via evictLRU-like
behavior; if no idle executor exists, do not evict active ones—instead fail fast
by throwing a clear error or return a specific failure so callers can handle
oversubscription (apply the same change to the other eviction site that uses
evictLRU()). Ensure you update evictLRU or add a new helper (e.g.,
evictIdleLRU()) so it only closes idle executors.

In `@packages/core/src/workflow/index.ts`:
- Around line 30-40: The example snippets that call z.string() are missing the
Zod import; update each snippet that uses z (e.g., the workflow example that
uses workflow(), toolStep(), createApp()) to include import { z } from "zod"; at
the top of the snippet so the z.symbols resolve and the examples are copy-paste
ready.

In `@packages/core/src/workflow/workflow-builder-impl.ts`:
- Around line 214-225: The step() implementation currently drops any provided
config for steps with type "parallel" or "branch"; update
WorkflowBuilderImpl.step to validate this by throwing a WorkflowValidationError
when config is passed for step types "parallel" or "branch" (instead of silently
ignoring it). Locate the step() method and the stepWithConfig merge logic, add
an early guard that checks if config is truthy && (step.type === "parallel" ||
step.type === "branch") and throw new WorkflowValidationError with a clear
message identifying the step name and invalid use of config; keep existing
behavior for other step types (merge config as before).
♻️ Duplicate comments (4)
packages/core/src/server/index.ts (1)

753-769: Internal tool calls still bypass middleware/hooks/output validation.

This matches a concern already raised on this PR (Line 753-769). Please route internal calls through the same execution pipeline as external calls.

packages/core/src/workflow/executor.ts (1)

431-465: Child steps still bypass per-step retry/timeout/onError policies.

executeParallelStep and executeBranchStep invoke executeStep directly, so nested StepConfig is ignored. This can silently drop retries, timeouts, or skip/error-handler behavior for child steps.

🔧 Proposed fix (route child steps through policy-aware execution)
@@
-    const results = await Promise.all(
-      step.steps.map((childStep) => this.executeStep(childStep, context))
-    );
+    const results = await Promise.all(
+      step.steps.map((childStep, index) =>
+        this.executeChildStep(`parallel[${index}]`, childStep, context)
+      )
+    );
@@
-    for (const childStep of stepsToExecute) {
-      const result = await this.executeStep(childStep, context);
+    for (const [index, childStep] of stepsToExecute.entries()) {
+      const result = await this.executeChildStep(`branch[${index}]`, childStep, context);
       results.push(result);
     }
@@
+  private async executeChildStep(
+    stepName: string,
+    step: Step,
+    context: WorkflowContext
+  ): Promise<unknown> {
+    try {
+      return await this.executeStepWithRetry(stepName, step, context, () => {});
+    } catch (error) {
+      const errorHandling = this.getErrorHandling(step);
+      if (errorHandling === "skip") return undefined;
+      if (typeof errorHandling === "function") {
+        return await errorHandling(error as Error, context);
+      }
+      throw new WorkflowExecutionError(
+        `Step "${stepName}" failed: ${(error as Error).message}`,
+        stepName,
+        { originalError: error }
+      );
+    }
+  }
packages/core/tests/workflow.test.ts (2)

194-202: Add Biome ignore comments for branch then properties.

Biome’s noThenProperty rule flags these configs; add an ignore comment above each branch config to keep the API intact while passing lint.

🛠️ Example fix (apply to each branch config)
     .branch("branch1", {
       when: (ctx) => (ctx.input as { value: string }).value === "test",
+      // biome-ignore lint/suspicious/noThenProperty: required by workflow branch API
       then: [customStep(async () => ({ result: "then" }))],
       else: [customStep(async () => ({ result: "else" }))],
     })

Also applies to: 328-337, 347-355, 365-375


482-517: Make the exponential-backoff retry test deterministic with fake timers.

Using real time can be flaky under CI load; drive retries with vi.useFakeTimers() and vi.advanceTimersByTimeAsync() instead.

🧪 Suggested adjustment
     it("should retry with exponential backoff", async () => {
       let attempts = 0;
       const delays: number[] = [];
-      let lastTime = Date.now();
+      vi.useFakeTimers();
+      let lastTime = Date.now();
@@
-      await wf.handler({ value: "test" }, mockToolContext);
+      try {
+        const run = wf.handler({ value: "test" }, mockToolContext);
+        await vi.advanceTimersByTimeAsync(10);
+        await vi.advanceTimersByTimeAsync(20);
+        await run;
+      } finally {
+        vi.useRealTimers();
+      }
🧹 Nitpick comments (1)
packages/core/src/workflow/workflow-builder-impl.ts (1)

329-362: Prefer defineTool for ToolDef construction

This inline ToolDef creation bypasses the standard helper used for inference and consistency. Please switch to defineTool (and defineUI if applicable) in the builder to align with the project’s type‑inference conventions.

As per coding guidelines, use defineTool/defineUI instead of inline objects.

Comment thread packages/core/src/server/index.ts
Comment thread packages/core/src/workflow/executor-manager.ts
Comment thread packages/core/src/workflow/index.ts
Comment thread packages/core/src/workflow/workflow-builder-impl.ts
… eviction

- Enhanced the server shutdown process to ensure the HTTP server always closes, even if an error occurs during executor shutdown.
- Updated ExecutorManager to evict only idle executors, preventing disruption to active workflows and providing clearer error messages when capacity is exceeded.
- Adjusted tests to reflect changes in error handling and eviction logic.

These improvements enhance the reliability and robustness of the workflow engine during shutdown scenarios.
@claude

claude Bot commented Jan 23, 2026

Copy link
Copy Markdown
Contributor

Workflow Engine PR Review

Summary

This is an excellent, well-architected feature addition that introduces a comprehensive workflow engine to the mcp-apps-kit. The implementation demonstrates strong engineering practices with thorough testing, proper error handling, and production-ready lifecycle management.

🎯 Strengths

Architecture & Design

  • Clean separation of concerns: Types, builder, executor, and manager are properly separated into focused modules
  • Fluent builder API: The workflow builder provides an intuitive, type-safe API that follows the repository's patterns
  • Smart environment detection: Automatic selection between traditional and edge executor managers is elegant
  • Resource management: Connection pooling, LRU eviction, and reference counting are well-implemented

Code Quality

  • Type safety: Extensive use of TypeScript generics and proper type inference throughout
  • Error handling: Comprehensive custom error classes (WorkflowDefinitionError, ToolResponseValidationError, etc.) with context
  • Validation: Proper use of Zod v4 for input/output validation with helpful error messages
  • Documentation: Excellent inline comments and JSDoc, especially in workflow/index.ts with production best practices

Testing

  • Comprehensive coverage: 663 lines in workflow.test.ts + 424 lines in executor-manager.test.ts
  • Proper test isolation: ExecutorManager reset between tests, cleanup in onTestFinished hooks
  • Real-world scenarios: Tests cover retry logic, parallel execution, conditional branching, error handling

Integration

  • Seamless integration: Workflows compile to standard ToolDef for transparent use with createApp
  • Internal tool calling: The _internalToolCaller pattern enables zero-config workflow composition
  • External MCP support: Both stdio and HTTP transports supported for external tools
  • UI integration: React widgets demonstrate proper integration with the UI system

🔍 Areas for Improvement

1. Race Condition in External Client Connection (Minor)

Location: packages/core/src/workflow/external-client.ts:186-214

The getOrCreateConnection method has good race condition handling with the pendingConnections map, but there's a subtle timing issue:

// Current code
let pending = this.pendingConnections.get(server);
if (!pending) {
  // Evict old connections if cache is full (before creating new promise)
  if (this.connections.size >= this.MAX_CONNECTIONS) {
    await this.evictOldConnections();  // ⚠️ Async operation before atomic store
  }
  
  pending = this.createConnectionWithTransport(server);
  this.pendingConnections.set(server, pending);
}

Issue: Between the await this.evictOldConnections() and setting pendingConnections, another concurrent call could create a duplicate connection.

Suggestion: Set a placeholder in pendingConnections before the eviction:

if (!pending) {
  const placeholder = Promise.resolve(); // Temporary marker
  this.pendingConnections.set(server, placeholder as any);
  
  if (this.connections.size >= this.MAX_CONNECTIONS) {
    await this.evictOldConnections();
  }
  
  pending = this.createConnectionWithTransport(server);
  this.pendingConnections.set(server, pending); // Replace placeholder
}

2. Memory Leak Risk in ExecutorManager (Medium)

Location: packages/core/src/workflow/executor-manager.ts:341-343

debugLogger.debug(`Evicting LRU idle executor: ${oldestKey}`);
// Fire and forget - don't block on cleanup
this.closeExecutor(oldestKey).catch(() => {
  // Ignore errors during eviction
});
return true;

Issue: The executor is removed from the map immediately in closeExecutor, but if closing fails, the external MCP connections may remain open. This could lead to connection leaks under failure scenarios.

Suggestion: Only remove from map after successful close:

private async closeExecutor(key: string): Promise<void> {
  const managed = this.executors.get(key);
  if (!managed) return;

  try {
    await managed.executor.close();
    this.executors.delete(key); // ✅ Delete only after successful close
  } catch (error) {
    debugLogger.error(`Error closing executor: ${key}`, { error });
    throw error;
  }
}

3. Validator Type Guard Could Be More Precise

Location: packages/core/src/workflow/executor.ts:42-48

function isZodSchema<T>(validator: Validator<T>): validator is z.ZodType<T> {
  return (
    typeof validator === "object" &&
    validator !== null &&
    "parse" in validator &&
    typeof validator.parse === "function"
  );
}

Issue: This type guard could false-positive on any object with a parse function method.

Suggestion: Use Zod's built-in check:

import { ZodType } from "zod";

function isZodSchema<T>(validator: Validator<T>): validator is z.ZodType<T> {
  return validator instanceof ZodType;
}

4. Server Shutdown Error Handling Could Be More Graceful

Location: packages/core/src/server/index.ts (around the server close logic)

Based on commit message: "Enhanced the server shutdown process to ensure the HTTP server always closes, even if an error occurs during executor shutdown."

Observation: While the improvement is good, consider logging which specific executors failed during shutdown for debugging. This would help production troubleshooting.

5. Missing Input Validation Documentation

Location: packages/core/src/workflow/external-client.ts:22-30

The toolInputSchema validates that input must be a plain object, but this restriction isn't clearly documented in the public API or JSDoc for externalStep().

Suggestion: Add to JSDoc:

/**
 * Create an external step that calls a tool from an external MCP server
 * 
 * @param toolName - Name of the tool to call on the external server
 * @param config - Step configuration
 * @returns External step definition
 * 
 * @remarks
 * Tool input must be a plain object (not null, array, or primitive).
 * ...
 */

✅ Security Review

  • No unsafe operations: No eval(), Function(), or command injection risks
  • Proper input validation: Zod schemas validate all inputs
  • Resource cleanup: Proper cleanup of external connections and timers
  • Error boundaries: Errors are caught and wrapped appropriately
  • No credential exposure: Debug logging doesn't expose sensitive data

📊 Test Coverage

The test suite is comprehensive with ~1170 test cases across the core package. The workflow-specific tests (workflow.test.ts + executor-manager.test.ts) cover:

  • ✅ Builder validation (description, input, steps required)
  • ✅ Sequential, parallel, and conditional execution
  • ✅ Retry logic with linear and exponential backoff
  • ✅ Error handling strategies (fail, skip, custom handlers)
  • ✅ Timeout handling
  • ✅ Reference counting and lifecycle management
  • ✅ Connection pooling and eviction
  • ✅ Edge vs traditional environment handling

Suggestion: Consider adding integration tests for:

  • Concurrent workflow executions under load
  • External MCP server connection failures and retries
  • Memory profiling for long-running workflows

🎨 Code Style & Conventions

The code adheres to the repository's standards:

  • ✅ TypeScript strict mode with no any types
  • ✅ Proper use of Zod v4 (not v3)
  • ✅ All exports flow through package index files
  • ✅ Consistent error handling patterns
  • ✅ Proper use of async/await without unhandled promises

📝 Documentation

The inline documentation is excellent, particularly:

  • Comprehensive workflow/index.ts with production deployment examples
  • Clear examples for both traditional servers and edge functions
  • JSDoc comments explaining complex logic
  • Helpful error messages with context

Minor suggestion: Add a workflow example to the main README.md or create a dedicated workflow guide in the docs/ folder.

🚀 Performance Considerations

Strengths:

  • Connection pooling reduces overhead for repeated external tool calls
  • LRU eviction prevents unbounded memory growth
  • Reference counting prevents premature resource cleanup
  • Automatic cleanup timers prevent connection leaks

Potential optimizations (for future consideration):

  • Consider adding circuit breaker pattern for failing external MCP servers
  • Add metrics/telemetry hooks for monitoring workflow performance in production
  • Consider adding a "warm-up" mode to pre-create frequently-used executors

🎯 Recommendation

APPROVE

This is production-ready code that significantly enhances the framework's capabilities. The minor issues identified above are not blockers and can be addressed in follow-up PRs if desired.

The workflow engine is well-architected, thoroughly tested, and properly integrated with the existing codebase. The automatic lifecycle management for different deployment environments shows thoughtful design for real-world usage.

Excellent work! 🎉


Priority of Issues:

  1. Medium Priority: Memory leak risk in executor cleanup (Create FUNDING.yml #2)
  2. Low Priority: Race condition in external client (Add Claude Code GitHub Workflow #1)
  3. Nice to Have: Type guard precision (Fix documentation misalignment in codebase #3), documentation improvements (Fix linting errors and warnings #4, Add GitHub Actions workflow for tests and linting #5)

- Introduced a new section in the README to describe the workflow engine, detailing its capabilities for composing multi-step workflows with features like parallel execution, conditional branching, and retry policies.
- Added TypeScript code examples demonstrating the creation and registration of workflows, including sequential steps, parallel execution, and error handling configurations.
- Updated the core README to include links to the new workflow engine documentation, enhancing the overall clarity and usability of the documentation.

These additions provide users with comprehensive guidance on utilizing the workflow engine effectively within their applications.
@claude

claude Bot commented Jan 23, 2026

Copy link
Copy Markdown
Contributor

Comprehensive Code Review - Workflow Engine Implementation

I've thoroughly reviewed this PR introducing a comprehensive workflow engine. This is a substantial and well-architected feature. Below is my detailed feedback:

Strengths

1. Excellent Architecture

  • Clean separation: types, errors, executor, builder, external client well-organized
  • Fluent builder API with type-safe state transitions
  • Comprehensive error hierarchy with context preservation
  • Production-ready lifecycle management (ExecutorManager, EdgeExecutorManager)

2. Strong Type Safety

  • Proper TypeScript strict mode usage
  • Generic type parameters flow correctly
  • No any types (adheres to repo policy)
  • Zod v4 schemas used correctly

3. Robust Error Handling

  • Three strategies: fail, skip, custom handlers
  • Retry logic with configurable backoff
  • Timeout support at step level
  • Error context preserved

4. Performance and Resource Management

  • Connection pooling with LRU eviction
  • Executor pooling and reuse
  • Reference counting prevents premature cleanup
  • Automatic TTL-based cleanup
  • Smart environment detection

5. Excellent Test Coverage

  • 32 comprehensive tests
  • Proper resource cleanup
  • Unique workflow names prevent pollution

CRITICAL Security Issue

External MCP Server Command Injection

Location: packages/core/src/workflow/external-client.ts:248-255

The createConnectionWithTransport method accepts arbitrary server identifiers without validation, allowing command injection:

Risk: A malicious server string could execute arbitrary commands.

Recommendation: Implement allowlist for permitted MCP servers. This MUST be fixed before merge.

Other Improvements

Potential Bugs

  1. Connection failures could benefit from exponential backoff
  2. Module-level cached edge manager could accumulate in long-lived runtimes

Performance

  1. Cleanup timer could use adaptive intervals
  2. Parallel steps: Promise.all cancels all if one fails - consider failFast option

Type Safety

Validator type guard uses duck-typing - could produce false positives

Test Coverage Gaps

  • Concurrent workflow execution
  • External tool connection failures
  • Executor manager edge cases
  • Memory/resource leak scenarios

Documentation

  • Add migration guide
  • Security warnings for external tools
  • Lifecycle documentation

Code Quality Summary

Category Rating Notes
Architecture 5/5 Excellent
Type Safety 4.5/5 Strong, minor concern
Error Handling 5/5 Comprehensive
Testing 4/5 Good, gaps exist
Security 3/5 Command injection issue
Performance 4/5 Well-optimized
Documentation 4.5/5 Excellent

Recommendations

Must Fix (P0):

  1. Implement allowlist for external MCP servers
  2. Add security warnings

Should Fix (P1):

  1. Add concurrent execution tests
  2. Improve type guard
  3. Document edge lifecycle

Nice to Have (P2):

  1. failFast option for parallel
  2. Adaptive cleanup intervals
  3. Migration guide

Overall Assessment

Excellent, production-ready implementation with deep understanding of TypeScript and async patterns. The command injection vulnerability must be addressed before merge.

Recommendation: Approve with required changes

Great work!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/core/README.md`:
- Around line 672-695: Update the documented import list and example to include
the missing ToolResponseValidationError type: add ToolResponseValidationError to
the import array alongside WorkflowError, WorkflowExecutionError,
StepTimeoutError, ExternalToolError, WorkflowValidationError, and
WorkflowDefinitionError, and include a brief example catch branch (e.g., if
(error instanceof ToolResponseValidationError) ...) demonstrating how to handle
tool response validation failures; reference the README's error list and the
Workflow error handling snippet where these symbols are shown.

Comment thread packages/core/README.md
Comment on lines +672 to +695
### Workflow Errors

The workflow engine provides specific error types for debugging:

```ts
import {
WorkflowError, // Base class for all workflow errors
WorkflowExecutionError, // Step execution failures
StepTimeoutError, // Step timeout exceeded
ExternalToolError, // External MCP call failures
WorkflowValidationError, // Input/output validation failures
WorkflowDefinitionError, // Invalid workflow configuration
} from "@mcp-apps-kit/core";

try {
await orderWorkflow.handler(input, context);
} catch (error) {
if (error instanceof StepTimeoutError) {
console.log(`Step timed out: ${error.stepName}`);
} else if (error instanceof ExternalToolError) {
console.log(`External call failed: ${error.serverUri}`);
}
}
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify workflow error types exist and match documentation

echo "=== Searching for all workflow error class definitions ==="
rg -n "class.*Error extends" packages/core/src/workflow/errors.ts

echo -e "\n=== Searching for ToolResponseValidationError ==="
rg -n "ToolResponseValidationError" packages/core/src/

echo -e "\n=== Searching for WorkflowValidationError ==="
rg -n "WorkflowValidationError" packages/core/src/

echo -e "\n=== Check error exports ==="
rg -n "export.*Error" packages/core/src/workflow/errors.ts

Repository: AndurilCode/mcp-apps-kit

Length of output: 2521


Documentation incomplete: ToolResponseValidationError is missing.

The ToolResponseValidationError error type exists in the implementation (packages/core/src/workflow/errors.ts and is exported from @mcp-apps-kit/core), but it is not included in the documented error types here. Update the import list and consider adding it to the error handling example.

Current documentation (incomplete)
import {
  WorkflowError,
  WorkflowExecutionError,
  StepTimeoutError,
  ExternalToolError,
  WorkflowValidationError,
  WorkflowDefinitionError,
} from "@mcp-apps-kit/core";
🤖 Prompt for AI Agents
In `@packages/core/README.md` around lines 672 - 695, Update the documented import
list and example to include the missing ToolResponseValidationError type: add
ToolResponseValidationError to the import array alongside WorkflowError,
WorkflowExecutionError, StepTimeoutError, ExternalToolError,
WorkflowValidationError, and WorkflowDefinitionError, and include a brief
example catch branch (e.g., if (error instanceof ToolResponseValidationError)
...) demonstrating how to handle tool response validation failures; reference
the README's error list and the Workflow error handling snippet where these
symbols are shown.

@gabrypavanello
gabrypavanello merged commit 32b9ecf into main Jan 23, 2026
6 checks passed
@gabrypavanello
gabrypavanello deleted the workflow-engine branch January 23, 2026 10:28
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