Skip to content

Conversation

@omeraplak
Copy link
Member

@omeraplak omeraplak commented Jan 8, 2026

PR Checklist

Please check if your PR fulfills the following requirements:

Bugs / Features

What is the current behavior?

What is the new behavior?

fixes (issue)

Notes for reviewers


Summary by cubic

Adds workflow control steps (branch, foreach, loop, map, sleep/sleepUntil), workflow guardrails and retry policies, and updated lifecycle hooks with step snapshots. Updates core API, serialization, tracing, examples, and docs.

  • New Features
    • andBranch: run all matching branches; returns results aligned to branch order.
    • andForEach: process array items with optional concurrency.
    • andDoWhile / andDoUntil: loop with simple conditions; runs at least once.
    • andMap: compose outputs from data, input, context, step results, or a function.
    • andSleep / andSleepUntil: pause by duration or until a date; respects suspend/cancel signals.
    • Guardrails: andGuardrail step and workflow-level input/output guardrails; supports strings/messages and structured data.
    • Workflow retries: set defaults via retryConfig; per-step overrides; execute receives retryCount.
    • Hooks: onStart/onSuspend/onError/onFinish/onEnd now provide step snapshots.
    • Serialization/tracing extended for new step types; tests and docs added; examples updated.

Written for commit 3ca150d. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • New workflow control steps: branch, foreach, map, do-while/do-until loops, sleep, sleep-until.
    • Guardrails for input/output with runtime support and guardrail agent integration.
    • Workflow-level retry policies and per-step retries; execute callbacks surface retryCount.
    • New terminal hooks: onSuspend, onError, onFinish (onEnd semantics updated).
  • Documentation

    • Docs, examples, and sidebar entries added for new steps, guardrails, retries, and hooks.
  • Tests

    • New unit tests covering branching, foreach, loops, map, sleep, guardrails, and hooks.

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

@changeset-bot
Copy link

changeset-bot bot commented Jan 8, 2026

🦋 Changeset detected

Latest commit: 3ca150d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@voltagent/core Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 8, 2026

📝 Walkthrough

Walkthrough

Adds workflow control primitives (sleep, sleep-until, foreach, loop, branch, map), guardrail runtime and steps, per-step and workflow retry propagation, abort-aware wait utilities, tracing helpers, expanded types/serialization, tests, examples, and docs.

Changes

Cohort / File(s) Summary
Core Step Implementations
packages/core/src/workflow/steps/and-sleep.ts, packages/core/src/workflow/steps/and-sleep-until.ts, packages/core/src/workflow/steps/and-foreach.ts, packages/core/src/workflow/steps/and-branch.ts, packages/core/src/workflow/steps/and-loop.ts, packages/core/src/workflow/steps/and-map.ts, packages/core/src/workflow/steps/and-guardrail.ts
New workflow step factories: sleep, sleep-until, foreach (concurrency-aware), branch, loop (do-while/do-until), map, and guardrail; include execution logic, tracing spans, sub-execution isolation, and guardrail invocation.
Step Helpers & Signals
packages/core/src/workflow/steps/helpers.ts, packages/core/src/workflow/steps/signal.ts
Extended matchStep to cover new step kinds; added waitWithSignal and throwIfAborted for abort-aware delays and canonical abort errors.
Workflow Chain API & Public Exports
packages/core/src/workflow/chain.ts, packages/core/src/workflow/steps/index.ts, packages/core/src/workflow/index.ts, packages/core/src/index.ts
Added fluent chain methods: andSleep, andSleepUntil, andForEach, andBranch, andDoWhile, andDoUntil, andMap, andGuardrail; re-exported new step factories and related types.
Types, Internal Shape & Node Types
packages/core/src/workflow/steps/types.ts, packages/core/src/workflow/types.ts, packages/core/src/workflow/internal/types.ts, packages/core/src/utils/node-utils.ts
New step type definitions and map entry/result types; added WorkflowRetryConfig, WorkflowStepData, expanded WorkflowStepType/NodeType unions; added per-step retries and execution retryCount.
Workflow Core, Guardrails & Serialization
packages/core/src/workflow/core.ts, packages/core/src/workflow/internal/guardrails.ts
Integrated workflow-level guardrail runtime, input/output guardrail application, guardrail agent wiring; extended serialization/state to include retries, sleep/loop/map metadata and guardrail counts; propagated retryConfig.
Execution Context & Utilities
packages/core/src/workflow/internal/utils.ts, packages/core/src/workflow/context.ts
Widened createStepExecutionContext signature to accept DATA directly with optional retryCount; WorkflowExecutionContext.stepData now uses WorkflowStepData; added optional guardrailAgent and expanded WorkflowStepContext.stepType union.
OpenTelemetry Tracing
packages/core/src/workflow/open-telemetry/trace-context.ts
Added createChildSpan and setInput helpers for child spans and root input attribute.
Tests & Mocks
packages/core/src/workflow/steps/*.spec.ts, packages/core/src/workflow/hooks.spec.ts, packages/core/src/workflow/guardrails.spec.ts, packages/core/src/test-utils/mocks/workflows.ts
New unit tests for branch, foreach, loop, map, sleep, guardrails, and hooks. Mock workflow context now includes optional writer with default mock functions.
Examples & Documentation
examples/with-workflow/src/index.ts, website/docs/workflows/steps/*.md, website/docs/workflows/execute-api.md, website/docs/workflows/overview.md, website/recipes/workflows.md, website/sidebars.ts, .changeset/*.md
New examples and docs for added steps, guardrails, and retry policies; execute API docs updated to include retryCount; hooks and sidebar updated.
Agent/Guardrail Types & Minor API tweaks
packages/core/src/agent/guardrail.ts, packages/core/src/agent/types.ts
normalizeOutputGuardrailList made generic; AgentEvalOperationType extended with "workflow".

Sequence Diagram(s)

sequenceDiagram
  participant Runner as Workflow Runner
  participant Step as Workflow Step
  participant Guard as Guardrail Runtime / Agent
  participant Tracer as Tracing
  participant Signal as AbortSignal
  participant Store as Persistence

  Runner->>Tracer: start span for step
  Runner->>Step: execute(context, retryCount=0)
  alt step applies input guardrails
    Step->>Guard: apply input guardrails
    Guard-->>Step: transformed input or block/error
  end
  alt sleeping or waiting
    Step->>Signal: waitWithSignal(delay, signal)
    Signal-->>Step: (abort) reason
  end
  alt step spawns sub-executions (foreach/branch/loop)
    Step->>Tracer: start child spans per sub-execution
    Step->>Step: execute sub-step(s) (isolated context)
    Step-->>Tracer: end child spans
  end
  alt error thrown
    Step->>Tracer: record error, end span
    Step-->>Runner: throw error
    alt retries available
      Runner->>Runner: apply retry delay
      Runner->>Step: execute(context, retryCount+1)
    end
  else success
    Step->>Guard: apply output guardrails (if any)
    Guard-->>Step: transformed output
    Step-->>Runner: return result
    Step->>Tracer: end span (success)
  end
  Runner->>Store: persist final/suspend state (as needed)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I hopped through branches, maps, and sleeps,
I counted loops where logic peeps,
For-each I twirled with careful pace,
Guardrails kept my data's face,
Retries nudged my steady leaps.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main feature addition: workflow control steps including branch, foreach, loop, map, and sleep.
Description check ✅ Passed The description includes an auto-generated summary with clear feature explanations, new capabilities, and infrastructure updates, but the PR checklist items are incomplete and unchecked.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 120c5a7 and 3ca150d.

📒 Files selected for processing (1)
  • packages/core/src/workflow/guardrails.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/workflow/guardrails.spec.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: Test core
  • GitHub Check: Build (Node 20)
  • GitHub Check: Build (Node 24)
  • GitHub Check: Build (Node 22)

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

@joggrbot

This comment has been minimized.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 35 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="website/docs/workflows/steps/and-branch.md">

<violation number="1" location="website/docs/workflows/steps/and-branch.md:17">
P2: Example uses mutually exclusive conditions that never both execute, failing to demonstrate andBranch's key feature: running multiple branches when multiple conditions are true. Consider using conditions like `amount > 1000` and `amount > 500` so readers can see both branches execute for values like 1500.</violation>
</file>

<file name=".changeset/social-humans-hammer.md">

<violation number="1" location=".changeset/social-humans-hammer.md:68">
P1: The example chains `andDoWhile` after `andForEach`, but `andForEach` returns an array while the loop logic expects a single number. After doubling `[1, 2, 3]` → `[2, 4, 6]`, the increment step `data + 1` and condition `data < 3` would operate on an array, not a number, causing a type error.</violation>

<violation number="2" location=".changeset/social-humans-hammer.md:117">
P1: The `date` parameter in `andSleepUntil` should be a function returning a Date, not a Date object. Using `new Date(Date.now() + 60_000)` will evaluate at workflow definition time, not execution time, causing the sleep to target a past timestamp if the workflow runs later.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Jan 9, 2026

Deploying voltagent with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3ca150d
Status: ✅  Deploy successful!
Preview URL: https://34e95595.voltagent.pages.dev
Branch Preview URL: https://feat-add-workflow-control-st.voltagent.pages.dev

View logs

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/core/src/utils/node-utils.ts (2)

52-63: Bug: getNodeTypeFromNodeId can’t parse NodeTypes containing underscores.

Example: nodeId starting with "workflow_map_step_" yields parts[0] === "workflow" and will never match "workflow_map_step".

Proposed fix
 export const getNodeTypeFromNodeId = (nodeId: string): NodeType | null => {
-  const parts = nodeId.split("_");
-  if (parts.length >= 1) {
-    const typePart = parts[0].toLowerCase();
-    for (const type of Object.values(NodeType)) {
-      if (typePart === type) {
-        return type as NodeType;
-      }
-    }
-  }
-  return null;
+  const lower = nodeId.toLowerCase();
+  for (const type of Object.values(NodeType)) {
+    const t = type.toLowerCase();
+    if (lower === t || lower.startsWith(`${t}_`)) {
+      return type as NodeType;
+    }
+  }
+  return null;
 };

200-213: Inconsistent: createWorkflowStepNodeId can embed stepName for all steps, but extractor only handles "func".

If node IDs for "sleep" | "branch" | ... include a name suffix, extractWorkflowStepInfo will currently drop it.

Proposed fix
   if (rest.length > 0) {
     const identifier = rest.join("_");
 
-    if (stepType === "agent") {
-      (result as any).agentId = identifier;
-    } else if (stepType === "func") {
-      (result as any).stepName = identifier;
-    } else if (identifier.startsWith("parallel_")) {
+    if (identifier.startsWith("parallel_")) {
       const parallelIndex = Number.parseInt(identifier.replace("parallel_", ""));
       if (!Number.isNaN(parallelIndex)) {
         (result as any).parallelIndex = parallelIndex;
       }
+    } else if (stepType === "agent") {
+      (result as any).agentId = identifier;
+    } else {
+      (result as any).stepName = identifier;
     }
   }
🤖 Fix all issues with AI agents
In @packages/core/src/workflow/steps/and-foreach.spec.ts:
- Around line 6-24: Add several unit tests for andForEach: keep the existing
happy-path using andForEach and andThen, then add tests that call step.execute
with an empty array (expect []), a single-item array, and mixed data types to
verify type behavior; add a test that sets the concurrency parameter on
andForEach and uses async inner steps to assert parallelism limits and order
preservation; add a test where the inner step (the andThen.execute
implementation) throws to assert error propagation/handling; and add a test
invoking step.execute with a non-array input to assert validation/rejection
behavior. Use createMockWorkflowExecuteContext to build inputs and reference
andForEach, andThen, step.execute, and the concurrency option so tests locate
the right implementation.

In @packages/core/src/workflow/steps/and-map.ts:
- Around line 46-74: resolveMapEntry currently returns entry.fn(context) without
awaiting, so async InternalWorkflowFunc results remain unresolved; change the
"fn" branch in resolveMapEntry to "return await entry.fn(context);" and ensure
callers (e.g., the andMap flow that collects map entries) await resolveMapEntry
(use Promise.all when mapping over entries) so the final result contains
resolved values not Promises; keep resolveMapEntry signature async and update
any usages that assumed synchronous returns.

In @packages/core/src/workflow/steps/and-sleep.spec.ts:
- Around line 7-20: Add more unit tests for andSleep: keep the existing
zero-duration case, add a positive-duration test (e.g., 100ms) that measures
elapsed time around step.execute created via createMockWorkflowExecuteContext to
assert it waited at least the duration, add invalid-input tests that pass
negative duration and non-numeric values to andSleep and assert it
rejects/throws, and consider using Jest fake timers or real timing checks
(Date.now() before/after) to verify actual delay; reference the andSleep factory
and the step.execute call to locate where to invoke and assert behavior.
- Around line 22-35: Add unit tests for andSleepUntil to cover missing
scenarios: create a test where the provided date is in the future and assert the
step delays execution until the target time (use Jest timers or a time-mocking
utility to advance time and verify step.execute resumes only after the target);
add tests for invalid inputs (null, undefined, new Date("invalid")) and confirm
the step rejects or returns expected error behavior; add tests for dynamic date
suppliers by passing a function (ctx => Date or async function) into
andSleepUntil and assert correct resolution and waiting behavior; include a
timing verification test that measures elapsed time (or uses fake timers) to
ensure the workflow pauses until the specified date; use the existing helpers
createMockWorkflowExecuteContext and the andSleepUntil() factory and assert on
step.execute results and thrown errors as appropriate.
🧹 Nitpick comments (12)
website/docs/workflows/steps/and-loop.md (1)

1-68: Documentation looks good and examples are clear.

The documentation effectively explains the do-while and do-until loop semantics with practical examples. The function signatures are clearly documented, and the notes section appropriately highlights that the step runs at least once.

Consider adding brief examples of:

  • Return value/output of the loop (what data flows to the next step)
  • How to handle errors within the loop condition or step
  • Common use cases (e.g., retry logic, polling)
📝 Optional enhancement: Add return value example

After line 63, you could add:

## Return Value

The loop returns the final result from the last execution of the step:

\```typescript
const result = await workflow.execute(0);
// result will be the final incremented value after the loop completes
\```
packages/core/src/workflow/steps/and-sleep.spec.ts (1)

6-6: Misleading describe block name.

The describe block is named "andSleep" but contains tests for both andSleep and andSleepUntil. Consider using a more inclusive name like "Sleep Steps" or split into separate describe blocks for each function.

♻️ Proposed fix
-describe("andSleep", () => {
+describe("Sleep Steps", () => {
+  describe("andSleep", () => {
   it("returns input data after sleeping", async () => {
     const step = andSleep({
       id: "sleep",
       duration: 0,
     });

     const result = await step.execute(
       createMockWorkflowExecuteContext({
         data: { ok: true },
       }),
     );

     expect(result).toEqual({ ok: true });
   });
+  });

+  describe("andSleepUntil", () => {
   it("returns input data when sleepUntil is in the past", async () => {
     const step = andSleepUntil({
       id: "sleep-until",
       date: new Date(Date.now() - 1000),
     });

     const result = await step.execute(
       createMockWorkflowExecuteContext({
         data: { ok: true },
       }),
     );

     expect(result).toEqual({ ok: true });
   });
+  });
 });
website/sidebars.ts (1)

120-128: Consider alphabetizing workflow step entries for better discoverability.

The new workflow steps are not in alphabetical order (e.g., "and-branch" appears before "and-tap", but "and-foreach" appears after "and-race"). Consistent alphabetical ordering would improve user navigation and make it easier to locate specific steps in the documentation sidebar.

📋 Proposed alphabetical ordering
        "workflows/steps/and-agent",
+       "workflows/steps/and-all",
+       "workflows/steps/and-branch",
+       "workflows/steps/and-foreach",
+       "workflows/steps/and-loop",
+       "workflows/steps/and-map",
+       "workflows/steps/and-race",
+       "workflows/steps/and-sleep",
+       "workflows/steps/and-sleep-until",
+       "workflows/steps/and-tap",
        "workflows/steps/and-when",
-       "workflows/steps/and-branch",
-       "workflows/steps/and-tap",
-       "workflows/steps/and-all",
-       "workflows/steps/and-race",
-       "workflows/steps/and-foreach",
-       "workflows/steps/and-loop",
-       "workflows/steps/and-sleep",
-       "workflows/steps/and-sleep-until",
-       "workflows/steps/and-map",
examples/with-workflow/src/index.ts (1)

486-503: Simplify branch result selection logic.

The branch result selection uses a broad type assertion and unnecessary find operation. Since the branches are mutually exclusive (counter >= 3 vs counter < 3), you can simplify this logic and improve type safety.

♻️ Proposed simplification
  .andThen({
    id: "select-branch",
    execute: async ({ data }) => {
-     const results = Array.isArray(data) ? data : [];
-     const selected = results.find((entry) => entry !== undefined) as
-       | { counter: number; label: "ready" | "warmup" }
-       | undefined;
-
-     if (!selected) {
-       return { counter: 0, label: "warmup" };
-     }
-
-     return {
-       counter: selected.counter,
-       label: selected.label,
-     };
+     // andBranch returns an array; find the first defined result
+     const results = Array.isArray(data) ? data : [];
+     const selected = results.find((entry) => entry !== undefined);
+     
+     // Fallback to default if no branch executed
+     return selected ?? { counter: 0, label: "warmup" as const };
    },
  });
packages/core/src/workflow/steps/and-loop.spec.ts (1)

6-44: Test logic is correct; consider edge case coverage.

Both test cases correctly verify the core loop semantics (do-while continues while condition is true; do-until continues until condition is true). The assertions match the expected behavior based on the provided loop configurations.

💡 Optional: Add edge case tests

Consider adding tests for:

  • Condition that's immediately false (do-while should still run once)
  • Error handling within the loop step
  • Loop with async condition evaluation
  • Maximum iteration safeguards (if implemented)

Example:

it("runs do-while at least once even if condition starts false", async () => {
  const step = andDoWhile({
    id: "loop",
    step: andThen({
      id: "increment",
      execute: async ({ data }) => data + 1,
    }),
    condition: async () => false,
  });

  const result = await step.execute(
    createMockWorkflowExecuteContext({ data: 0 })
  );

  expect(result).toBe(1); // Ran once despite false condition
});

Based on learnings, ensure all tests pass before committing.

website/docs/workflows/steps/and-branch.md (1)

1-52: Clarify multi-branch execution behavior and add output example.

The documentation provides a solid foundation, but could be enhanced for clarity:

  1. The description states "All branches whose condition is true will execute," but doesn't specify whether execution is parallel or sequential.
  2. The example shows two branches, but doesn't demonstrate an output where one or both branches run (or don't run).
  3. The Notes section mentions the array structure and undefined values but would benefit from a concrete example.
💡 Suggested enhancement

Consider adding an "Output" section after the Quick Start example:

## Example Output

When executed with `{ amount: 1500 }`:
```typescript
[
  { amount: 1500, large: true },  // First branch ran
  undefined                        // Second branch didn't run
]
```

When executed with `{ amount: -50 }`:
```typescript
[
  undefined,                          // First branch didn't run  
  { amount: -50, invalid: true }      // Second branch ran
]
```

Also clarify in the description whether branches execute in parallel or sequentially (e.g., "All branches whose condition is true will execute in parallel").

packages/core/src/workflow/steps/and-map.spec.ts (1)

5-38: Comprehensive test for all map source types; consider edge cases.

The test thoroughly exercises all five map source types (data, input, context, step, value) and correctly validates the aggregated output object. The mock setup is well-structured with appropriate test data for each source.

💡 Optional: Add edge case tests

Consider adding tests for error conditions and edge cases:

it("handles missing paths gracefully", async () => {
  const step = andMap({
    id: "map",
    map: {
      missing: { source: "data", path: "nonExistent" },
    },
  });

  const result = await step.execute(
    createMockWorkflowExecuteContext({ data: {} })
  );

  expect(result.missing).toBeUndefined();
});

it("handles non-existent step IDs", async () => {
  const step = andMap({
    id: "map",
    map: {
      fromStep: { source: "step", stepId: "does-not-exist", path: "value" },
    },
  });

  const result = await step.execute(
    createMockWorkflowExecuteContext({
      getStepData: () => undefined,
    })
  );

  expect(result.fromStep).toBeUndefined();
});

Based on learnings, ensure all tests pass before committing.

packages/core/src/workflow/steps/and-sleep.ts (1)

17-30: Small type/validation polish for duration and return value.

  • Consider normalizing/validating durationMs here (and optionally throwing on non-finite), rather than silently deferring to waitWithSignal’s clamp-to-0 behavior.
  • return context.data as DATA; should be redundant if context is properly typed as WorkflowExecuteContext<INPUT, DATA, ...>.
packages/core/src/workflow/steps/and-map.ts (2)

10-27: readPath edge-cases: leading dots / empty segments / optional chaining behavior.

Today, paths like ".foo" produce an empty first segment, and missing mid-path segments throw "Invalid path ...". If you want “undefined when missing” semantics (or to tolerate leading dots), this function needs adjustment.


87-99: Optional: consider parallel resolution of independent map entries.

If map entries are independent, Promise.all(entries.map(...)) can reduce latency vs serial for...of.

packages/core/src/workflow/steps/signal.ts (1)

1-56: Abortable wait + cleanup looks good.

One small consideration: if downstream relies on structured error typing, you may want a dedicated error type/shape instead of encoding "WORKFLOW_CANCELLED" / "WORKFLOW_SUSPENDED" in Error.message.

packages/core/src/workflow/steps/types.ts (1)

180-181: Clarify branch execution semantics in documentation.

The result type Array<RESULT | undefined> suggests all branches are evaluated concurrently, with undefined for non-matching conditions. This is a valid multi-branch pattern but may differ from typical "first-match-wins" branching semantics users expect.

Ensure documentation clearly explains that multiple branches can execute and return results simultaneously.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2712078 and 2101d45.

📒 Files selected for processing (35)
  • .changeset/social-humans-hammer.md
  • examples/with-workflow/src/index.ts
  • packages/core/src/index.ts
  • packages/core/src/test-utils/mocks/workflows.ts
  • packages/core/src/utils/node-utils.ts
  • packages/core/src/workflow/chain.ts
  • packages/core/src/workflow/context.ts
  • packages/core/src/workflow/core.ts
  • packages/core/src/workflow/index.ts
  • packages/core/src/workflow/internal/types.ts
  • packages/core/src/workflow/internal/utils.ts
  • packages/core/src/workflow/steps/and-branch.spec.ts
  • packages/core/src/workflow/steps/and-branch.ts
  • packages/core/src/workflow/steps/and-foreach.spec.ts
  • packages/core/src/workflow/steps/and-foreach.ts
  • packages/core/src/workflow/steps/and-loop.spec.ts
  • packages/core/src/workflow/steps/and-loop.ts
  • packages/core/src/workflow/steps/and-map.spec.ts
  • packages/core/src/workflow/steps/and-map.ts
  • packages/core/src/workflow/steps/and-sleep-until.ts
  • packages/core/src/workflow/steps/and-sleep.spec.ts
  • packages/core/src/workflow/steps/and-sleep.ts
  • packages/core/src/workflow/steps/helpers.ts
  • packages/core/src/workflow/steps/index.ts
  • packages/core/src/workflow/steps/signal.ts
  • packages/core/src/workflow/steps/types.ts
  • packages/core/src/workflow/types.ts
  • website/docs/workflows/steps/and-branch.md
  • website/docs/workflows/steps/and-foreach.md
  • website/docs/workflows/steps/and-loop.md
  • website/docs/workflows/steps/and-map.md
  • website/docs/workflows/steps/and-sleep-until.md
  • website/docs/workflows/steps/and-sleep.md
  • website/recipes/workflows.md
  • website/sidebars.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: Maintain type safety in TypeScript-first codebase
Never use JSON.stringify; use the safeStringify function instead, imported from @voltagent/internal

Files:

  • packages/core/src/workflow/steps/signal.ts
  • packages/core/src/workflow/steps/and-sleep.spec.ts
  • packages/core/src/workflow/steps/and-sleep-until.ts
  • packages/core/src/workflow/steps/and-branch.ts
  • packages/core/src/index.ts
  • packages/core/src/workflow/internal/types.ts
  • packages/core/src/workflow/context.ts
  • packages/core/src/workflow/steps/and-map.ts
  • packages/core/src/workflow/types.ts
  • website/sidebars.ts
  • packages/core/src/workflow/internal/utils.ts
  • packages/core/src/workflow/steps/and-foreach.ts
  • packages/core/src/workflow/steps/and-foreach.spec.ts
  • packages/core/src/workflow/steps/and-loop.ts
  • packages/core/src/workflow/steps/and-sleep.ts
  • packages/core/src/workflow/core.ts
  • packages/core/src/workflow/steps/helpers.ts
  • packages/core/src/workflow/steps/and-map.spec.ts
  • packages/core/src/utils/node-utils.ts
  • packages/core/src/workflow/steps/and-loop.spec.ts
  • packages/core/src/workflow/index.ts
  • packages/core/src/test-utils/mocks/workflows.ts
  • packages/core/src/workflow/steps/and-branch.spec.ts
  • packages/core/src/workflow/chain.ts
  • packages/core/src/workflow/steps/index.ts
  • examples/with-workflow/src/index.ts
  • packages/core/src/workflow/steps/types.ts
🧠 Learnings (1)
📚 Learning: 2026-01-07T05:09:23.217Z
Learnt from: CR
Repo: VoltAgent/voltagent PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-07T05:09:23.217Z
Learning: Applies to **/*.test.ts : Test your changes - ensure all tests pass before committing

Applied to files:

  • packages/core/src/workflow/steps/and-sleep.spec.ts
  • packages/core/src/workflow/steps/and-foreach.spec.ts
  • packages/core/src/workflow/steps/and-map.spec.ts
  • packages/core/src/workflow/steps/and-loop.spec.ts
  • packages/core/src/workflow/steps/and-branch.spec.ts
🧬 Code graph analysis (11)
packages/core/src/workflow/steps/and-sleep-until.ts (5)
packages/core/src/workflow/chain.ts (1)
  • andSleepUntil (562-567)
packages/core/src/workflow/steps/types.ts (2)
  • WorkflowStepSleepUntilConfig (138-140)
  • WorkflowStepSleepUntil (142-146)
packages/core/src/workflow/internal/utils.ts (1)
  • defaultStepConfig (54-60)
packages/core/src/workflow/internal/types.ts (1)
  • WorkflowExecuteContext (41-57)
packages/core/src/workflow/steps/signal.ts (1)
  • waitWithSignal (26-56)
packages/core/src/workflow/steps/and-branch.ts (5)
packages/core/src/workflow/chain.ts (1)
  • andBranch (572-589)
packages/core/src/workflow/steps/types.ts (2)
  • WorkflowStepBranchConfig (173-178)
  • WorkflowStepBranch (180-187)
packages/core/src/workflow/internal/utils.ts (1)
  • defaultStepConfig (54-60)
packages/core/src/workflow/steps/signal.ts (1)
  • throwIfAborted (19-24)
packages/core/src/workflow/steps/helpers.ts (1)
  • matchStep (9-29)
packages/core/src/workflow/steps/and-map.ts (7)
packages/core/src/index.ts (2)
  • context (110-110)
  • andMap (17-17)
packages/core/src/workflow/index.ts (2)
  • WorkflowExecuteContext (33-33)
  • andMap (15-15)
packages/core/src/workflow/internal/types.ts (1)
  • WorkflowExecuteContext (41-57)
packages/core/src/workflow/steps/index.ts (4)
  • WorkflowStepMapEntry (32-32)
  • andMap (12-12)
  • WorkflowStepMapConfig (31-31)
  • WorkflowStepMapResult (33-33)
packages/core/src/workflow/steps/types.ts (4)
  • WorkflowStepMapEntry (189-195)
  • WorkflowStepMapConfig (207-215)
  • WorkflowStepMapResult (203-205)
  • WorkflowStepMap (217-224)
packages/core/src/workflow/chain.ts (1)
  • andMap (642-661)
packages/core/src/workflow/internal/utils.ts (1)
  • defaultStepConfig (54-60)
packages/core/src/workflow/steps/and-loop.ts (3)
packages/core/src/workflow/steps/helpers.ts (1)
  • matchStep (9-29)
packages/core/src/workflow/internal/utils.ts (1)
  • defaultStepConfig (54-60)
packages/core/src/workflow/steps/signal.ts (1)
  • throwIfAborted (19-24)
packages/core/src/workflow/core.ts (1)
packages/core/src/workflow/steps/types.ts (1)
  • WorkflowStep (234-247)
packages/core/src/workflow/steps/and-map.spec.ts (6)
packages/core/src/index.ts (1)
  • andMap (17-17)
packages/core/src/workflow/chain.ts (1)
  • andMap (642-661)
packages/core/src/workflow/index.ts (1)
  • andMap (15-15)
packages/core/src/workflow/steps/and-map.ts (1)
  • andMap (79-101)
packages/core/src/workflow/steps/index.ts (1)
  • andMap (12-12)
packages/core/src/test-utils/mocks/workflows.ts (1)
  • createMockWorkflowExecuteContext (19-41)
packages/core/src/workflow/steps/and-loop.spec.ts (3)
packages/core/src/workflow/chain.ts (2)
  • andDoWhile (610-621)
  • andDoUntil (626-637)
packages/core/src/workflow/steps/and-loop.ts (2)
  • andDoWhile (97-101)
  • andDoUntil (106-110)
packages/core/src/test-utils/mocks/workflows.ts (1)
  • createMockWorkflowExecuteContext (19-41)
packages/core/src/workflow/steps/and-branch.spec.ts (6)
packages/core/src/index.ts (2)
  • andBranch (14-14)
  • andThen (6-6)
packages/core/src/workflow/chain.ts (2)
  • andBranch (572-589)
  • andThen (363-369)
packages/core/src/workflow/index.ts (2)
  • andBranch (12-12)
  • andThen (3-3)
packages/core/src/workflow/steps/and-branch.ts (1)
  • andBranch (10-91)
packages/core/src/workflow/steps/index.ts (2)
  • andBranch (10-10)
  • andThen (2-2)
packages/core/src/test-utils/mocks/workflows.ts (1)
  • createMockWorkflowExecuteContext (19-41)
packages/core/src/workflow/chain.ts (6)
packages/core/src/workflow/steps/and-sleep.ts (1)
  • andSleep (17-31)
packages/core/src/workflow/steps/and-sleep-until.ts (1)
  • andSleepUntil (17-37)
packages/core/src/workflow/steps/and-branch.ts (1)
  • andBranch (10-91)
packages/core/src/workflow/steps/and-foreach.ts (1)
  • andForEach (10-112)
packages/core/src/workflow/steps/and-loop.ts (2)
  • andDoWhile (97-101)
  • andDoUntil (106-110)
packages/core/src/workflow/steps/and-map.ts (1)
  • andMap (79-101)
examples/with-workflow/src/index.ts (3)
packages/core/src/index.ts (2)
  • createWorkflowChain (3-3)
  • andThen (6-6)
packages/core/src/workflow/chain.ts (2)
  • createWorkflowChain (906-919)
  • andThen (363-369)
packages/core/src/workflow/steps/and-then.ts (1)
  • andThen (31-85)
packages/core/src/workflow/steps/types.ts (2)
packages/core/src/workflow/internal/types.ts (4)
  • InternalWorkflowStepConfig (68-82)
  • InternalWorkflowFunc (64-66)
  • InternalBaseWorkflowStep (88-129)
  • InternalAnyWorkflowStep (135-143)
packages/core/src/workflow/types.ts (1)
  • Workflow (379-466)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: Build (Node 22)
  • GitHub Check: Build (Node 24)
  • GitHub Check: Build (Node 20)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (44)
packages/core/src/test-utils/mocks/workflows.ts (1)

36-39: LGTM! Good addition for testing stream capabilities.

The mock writer follows the same pattern as the existing logger mock and provides appropriate test doubles for the write and pipeFrom methods. This enables testing of streaming functionality in the new workflow steps.

packages/core/src/workflow/steps/helpers.ts (1)

15-25: LGTM! Pattern matching correctly extended for new step types.

The matchStep function now handles all the new workflow control steps introduced in this PR: tap, workflow, sleep, sleep-until, foreach, loop, branch, and map. The implementation follows the existing pattern consistently.

packages/core/src/workflow/internal/utils.ts (1)

70-76: Type widening is safe at the current call site.

The function parameter now accepts plain DATA instead of InternalExtractWorkflowInputData<DATA>. While this technically widens the accepted input types, there is only one call site (core.ts:1362) and it binds DATA to typeof stateManager.state.data, which is typed as DangerouslyAllowAny. The type narrowing provided by InternalExtractWorkflowInputData<DATA> has no practical effect when applied to DangerouslyAllowAny, so the removal does not compromise type safety.

packages/core/src/workflow/internal/types.ts (1)

41-42: The type change is correct and poses no issues. WorkflowInput<INPUT_SCHEMA> and InternalExtractWorkflowInputData<T> have identical conditional logic, making the simplification from InternalExtractWorkflowInputData<DATA> to DATA semantically equivalent. All step implementations already work correctly—they receive properly typed DATA from WorkflowInput in the first step and maintain correct type inference through the step chain (DATA → RESULT → next step's DATA). Input validation occurs at the workflow.run level and is unaffected by the context type change. Existing test cases confirm proper type flow (e.g., ctx.data.count).

website/recipes/workflows.md (1)

78-93: LGTM! Comprehensive workflow methods table.

The expanded table clearly documents all available workflow control methods with concise descriptions. The formatting is consistent and the new methods (branch, foreach, loop, map, sleep) are well-integrated with existing ones.

website/docs/workflows/steps/and-foreach.md (1)

1-39: LGTM! Clear and complete documentation.

The documentation effectively explains the andForEach step with:

  • A practical quick-start example
  • Complete function signature with required and optional parameters
  • Important behavioral notes about array input, order preservation, and concurrency
website/docs/workflows/steps/and-sleep-until.md (1)

1-39: LGTM! Well-documented sleep-until step.

The documentation provides:

  • A clear quick-start example with a future date
  • Complete function signature showing both static Date and dynamic function options
  • Important notes about data passthrough and past-date behavior
website/docs/workflows/steps/and-sleep.md (1)

1-39: LGTM! Well-structured documentation.

The documentation is clear, concise, and complete. The Quick Start example demonstrates basic usage, and the function signature accurately reflects both static and computed duration options. The notes appropriately document signal handling behavior.

examples/with-workflow/src/index.ts (4)

2-2: LGTM! Required import for new workflow examples.

The andThen import is correctly added to support the new workflow examples that use it as a step factory within andForEach, andDoWhile, andDoUntil, and andBranch.


382-385: Verify date calculation doesn't target past time.

The andSleepUntil date is computed as Date.now() + 1000, which could potentially be in the past if the preceding andSleep step takes longer than expected or if there's any delay in execution. While this is an example/demo, it could mislead users about proper usage patterns.

Consider using a relative future time that accounts for the previous sleep duration, or document this limitation in comments:

.andSleepUntil({
  id: "align-to-next-second",
  // Ensure we're always targeting a future time
  date: () => new Date(Date.now() + 1500),
})

399-433: LGTM! Well-designed example demonstrating multiple control steps.

Example 6 effectively demonstrates andForEach with concurrency control and andMap with multiple source types. The defensive Array.isArray checks in the fn mappings (lines 426, 430) are good practice.


525-527: LGTM! New workflows correctly registered.

The three new example workflows are properly registered with the VoltAgent instance alongside existing workflows.

website/docs/workflows/steps/and-map.md (1)

1-53: LGTM! Comprehensive and clear documentation.

The documentation effectively demonstrates all mapping source types (value, data, input, context, step, fn) with a practical example. The function signature is complete, and the notes clarify the distinction between data and input sources.

packages/core/src/workflow/steps/and-loop.spec.ts (1)

1-5: LGTM! Test setup is clean and well-organized.

The imports and test structure follow project conventions appropriately.

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

92-105: LGTM! Step type union correctly expanded.

The stepType union has been properly extended to include all new workflow control steps introduced in this PR: sleep, sleep-until, foreach, loop, branch, map, plus tap and workflow for step composition. This aligns with the new step implementations and maintains type safety across the workflow system.

packages/core/src/workflow/steps/and-map.spec.ts (1)

1-4: LGTM! Clean test imports.

The test imports are well-organized and follow project conventions.

packages/core/src/index.ts (1)

11-17: LGTM! All new workflow control steps properly exported.

The public API surface has been correctly extended with all seven new workflow control steps:

  • Timing control: andSleep, andSleepUntil
  • Iteration: andForEach, andDoWhile, andDoUntil
  • Branching: andBranch
  • Data transformation: andMap

The exports follow the existing pattern and are appropriately placed within the workflow exports block.

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

1-16: Exports look consistent with the new step surface.

No concerns on the re-export wiring.

packages/core/src/workflow/steps/and-branch.ts (1)

20-66: Verify workflowContext clearing + parentStepId assumptions in branch execution.

  • subState.workflowContext = undefined means inner steps won’t see workflowContext (incl. any nested tracing), even though you wrap execution with traceContext.withSpan(...). Please confirm this is intentional and doesn’t break other workflowContext consumers.
  • parentStepId: config.id — if id can be absent, ensure createStepSpan tolerates it (or fallback to finalStep.id / a generated parent id).
packages/core/src/utils/node-utils.ts (1)

68-81: New step type mappings look complete.

The WorkflowStepType union and getWorkflowStepNodeType switch cover the newly introduced step kinds.

Also applies to: 132-163

.changeset/social-humans-hammer.md (1)

1-6: [No action needed—changeset bump level is correct per project convention.]

Your project consistently marks feature additions as patch releases, as evidenced by recent releases (v2.0.7, v2.0.4, v2.0.2, v2.0.1) in CHANGELOG.md. The patch designation for the new workflow control steps follows your established versioning practice.

packages/core/src/workflow/types.ts (2)

490-503: LGTM!

The expanded stepType union in BaseWorkflowStepHistoryEntry correctly includes all new workflow control step types and is consistent with the corresponding union in WorkflowStreamEvent.


642-648: LGTM!

The stepType union in WorkflowStreamEvent is properly aligned with the BaseWorkflowStepHistoryEntry type, ensuring consistency across the workflow system.

packages/core/src/workflow/steps/and-sleep-until.ts (1)

17-36: LGTM!

The implementation correctly:

  • Supports both static Date and dynamic function-based date resolution
  • Validates the target date with proper instanceof Date and NaN checks
  • Handles past dates gracefully (negative delayMs is clamped to 0 by waitWithSignal)
  • Respects workflow abort signals for interruptible sleep
packages/core/src/workflow/steps/and-loop.ts (2)

9-92: LGTM!

The loop implementation is well-structured:

  • Proper abort signal checks before and after each iteration ensure responsive cancellation
  • Per-iteration OpenTelemetry spans provide good observability
  • The subState correctly isolates nested tracing context
  • The condition evaluation logic at line 84 correctly implements:
    • do-while: executes at least once, continues while condition is true
    • do-until: executes at least once, continues until condition becomes true

97-110: LGTM!

Clean public API for both andDoWhile and andDoUntil that delegates to the shared createLoopStep helper.

packages/core/src/workflow/steps/and-foreach.ts (1)

10-111: LGTM!

Well-implemented forEach step with:

  • Proper input validation ensuring array data
  • Efficient short-circuit for empty arrays
  • Robust concurrency normalization handling edge cases (NaN, Infinity)
  • Safe concurrent execution pattern - the index claim (const index = nextIndex; nextIndex += 1;) happens synchronously before await, preventing race conditions
  • Per-item tracing spans for observability
  • Results correctly ordered by input index regardless of completion order
packages/core/src/workflow/steps/index.ts (2)

7-12: LGTM!

New step implementations are properly exported, maintaining consistency with the existing export structure.


26-33: LGTM!

Associated type exports are complete, enabling consumers to properly type their workflow configurations.

packages/core/src/workflow/core.ts (4)

2164-2189: LGTM!

The SerializedWorkflowStep interface is properly extended with all necessary fields for the new step types, providing a complete serialization schema.


2270-2298: LGTM!

Serialization for sleep and sleep-until steps correctly handles both static values and dynamic functions, converting functions to strings for storage/inspection.


2300-2334: LGTM!

Serialization for foreach and loop steps correctly captures nested step structure, concurrency settings, condition functions, and loop type metadata.


2336-2383: LGTM!

Good implementation:

  • Branch serialization correctly captures all branch conditions as function strings
  • Map serialization properly uses safeStringify per coding guidelines
  • Workflow step serialization correctly propagates the nested workflowId
packages/core/src/workflow/chain.ts (3)

17-42: LGTM!

Import section is properly updated with all necessary type imports for the new step configurations and builders.


549-567: LGTM!

andSleep and andSleepUntil chain methods correctly preserve CURRENT_DATA as the output type, since sleep operations don't transform the workflow data.


569-661: LGTM!

The new chain builder methods are well-designed:

  • andBranch: Returns Array<NEW_DATA | undefined> correctly modeling that not all branches may execute
  • andForEach: Returns NEW_DATA[] for the array of transformed items
  • andDoWhile/andDoUntil: Return NEW_DATA as the final loop result
  • andMap: Returns WorkflowStepMapResult<MAP> providing strongly-typed key-value results

The type casts via as unknown as WorkflowChain<...> are an acceptable pattern for fluent generic APIs in TypeScript.

packages/core/src/workflow/steps/types.ts (8)

14-27: LGTM!

The WorkflowStepType union correctly includes all new step type discriminants following the existing kebab-case naming convention.


128-136: LGTM!

Sleep step correctly implements pass-through semantics (returning DATA) and supports both static and dynamic duration values.


138-146: LGTM!

SleepUntil step is consistent with the Sleep step pattern and provides flexible date-based scheduling.


148-158: LGTM!

ForEach step correctly models array transformation with ITEM[] → RESULT[] semantics. The optional concurrency parameter provides useful parallel execution control.


160-171: LGTM!

Loop step correctly supports both do-while and do-until semantics via the loopType discriminant. The condition function appropriately receives RESULT for post-iteration evaluation.


189-224: LGTM!

The map step types are well-designed with comprehensive entry sources and appropriate type inference. The conditional type WorkflowStepMapEntryResult correctly infers VALUE for static values and return types for functions, falling back to unknown for path-based access where static typing isn't possible.


241-247: LGTM!

The union correctly includes all new step types. The use of any for WorkflowStepForEach type parameters is a pragmatic choice for union flexibility, avoiding complex variance issues while maintaining runtime type discrimination via the type property.


252-264: All usages of InternalWorkflow.run in the codebase are already updated and consistent with the DATA input type. The change from accepting INPUT to DATA is intentional—the _INPUT parameter (marked unused) indicates the internal API deliberately uses the current state type rather than the original input type. This is not a breaking change, and all call sites properly pass DATA-typed values.

Likely an incorrect or invalid review comment.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 @website/docs/workflows/steps/and-all.md:
- Line 65: Add a new "Retries" section to
website/docs/workflows/steps/and-all.md documenting the retries?: number
parameter: show a short example using retries: 2 and demonstrating how
retryCount is incremented on each thrown error, state that retries only apply to
thrown errors (suspend/cancel do not trigger retries), and explain interaction
with workflow-wide retryConfig by stating that per-step retries overrides the
workflow retryConfig for that step and how they combine/behave (e.g., per-step
value wins, default falls back to retryConfig if undefined). Ensure the section
follows the style of and-then.md (example + bullet points covering behavior and
override semantics).
🧹 Nitpick comments (1)
website/docs/workflows/steps/and-race.md (1)

68-75: Consider adding an example demonstrating the retries field.

The retries parameter is documented in the function signature but not shown in any example. Adding a simple code snippet showing how to use retries would help users understand this feature.

Optional: Example demonstrating retries usage
.andRace({
  id: "race-with-retries",
  steps: [
    andThen({ id: "cache", execute: async () => { /* ... */ } }),
    andThen({ id: "database", execute: async () => { /* ... */ } }),
  ],
  retries: 2  // Retry the entire race up to 2 times if all steps fail
})
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2101d45 and e135ab8.

📒 Files selected for processing (18)
  • .changeset/social-humans-hammer.md
  • packages/core/src/workflow/core.ts
  • packages/core/src/workflow/index.ts
  • packages/core/src/workflow/internal/types.ts
  • packages/core/src/workflow/internal/utils.ts
  • packages/core/src/workflow/types.ts
  • website/docs/workflows/execute-api.md
  • website/docs/workflows/overview.md
  • website/docs/workflows/steps/and-all.md
  • website/docs/workflows/steps/and-branch.md
  • website/docs/workflows/steps/and-foreach.md
  • website/docs/workflows/steps/and-loop.md
  • website/docs/workflows/steps/and-map.md
  • website/docs/workflows/steps/and-race.md
  • website/docs/workflows/steps/and-sleep-until.md
  • website/docs/workflows/steps/and-sleep.md
  • website/docs/workflows/steps/and-then.md
  • website/docs/workflows/steps/and-when.md
✅ Files skipped from review due to trivial changes (1)
  • website/docs/workflows/steps/and-sleep-until.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/core/src/workflow/internal/types.ts
  • website/docs/workflows/steps/and-loop.md
  • packages/core/src/workflow/index.ts
  • website/docs/workflows/steps/and-branch.md
  • packages/core/src/workflow/internal/utils.ts
  • website/docs/workflows/steps/and-sleep.md
🧰 Additional context used
📓 Path-based instructions (1)
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: Maintain type safety in TypeScript-first codebase
Never use JSON.stringify; use the safeStringify function instead, imported from @voltagent/internal

Files:

  • packages/core/src/workflow/types.ts
  • packages/core/src/workflow/core.ts
🧬 Code graph analysis (1)
packages/core/src/workflow/core.ts (5)
packages/core/src/workflow/types.ts (2)
  • WorkflowExecutionResult (136-158)
  • WorkflowSuspensionMetadata (16-34)
packages/core/src/workflow/index.ts (2)
  • WorkflowSuspensionMetadata (29-29)
  • serializeWorkflowStep (17-17)
packages/core/src/workflow/stream.ts (1)
  • NoOpWorkflowStreamWriter (91-109)
packages/core/src/workflow/internal/utils.ts (1)
  • createStepExecutionContext (70-88)
packages/core/src/workflow/steps/signal.ts (1)
  • waitWithSignal (26-56)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: Test core
  • GitHub Check: Test libsql
  • GitHub Check: Test supabase
  • GitHub Check: Build (Node 22)
  • GitHub Check: Build (Node 24)
  • GitHub Check: Build (Node 20)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (26)
website/docs/workflows/steps/and-race.md (1)

16-47: ✓ Documentation accurately reflects the new object-based API.

All examples consistently use the updated signature with id, steps, and optional fields. The function signature block clearly documents the new shape. Code examples are well-structured, realistic, and syntactically correct.

Also applies to: 68-75, 84-104, 112-131, 139-161, 169-192, 201-208, 217-224, 230-230, 246-253

website/docs/workflows/steps/and-then.md (2)

45-50: LGTM!

The documentation clearly describes the retryCount parameter and its behavior.


53-70: LGTM!

The Retries section provides clear documentation with practical examples and important behavioral notes.

packages/core/src/workflow/types.ts (3)

202-213: LGTM!

The WorkflowRetryConfig interface is well-defined with clear documentation and sensible defaults.


263-266: LGTM!

The retryConfig field is consistently added across all relevant interfaces with proper documentation.

Also applies to: 391-394, 447-450, 465-465


516-529: LGTM!

The stepType unions are consistently expanded to include all new workflow control steps across both BaseWorkflowStepHistoryEntry and WorkflowStreamEvent.

Also applies to: 661-674

packages/core/src/workflow/core.ts (8)

22-22: LGTM!

The import of waitWithSignal and destructuring of retryConfig are appropriate for the retry feature implementation.

Also applies to: 633-633


843-843: LGTM!

The retry configuration calculations are robust with proper validation, type checking, and fallback handling. The precedence logic (execution options > workflow config) is correct.

Also applies to: 1008-1014, 1027-1029


1270-1275: LGTM!

The span attributes are properly constructed with conditional inclusion of retry-related metadata for observability.


1346-1439: LGTM!

The handleStepSuspension function properly handles all aspects of step suspension: span ending, metadata creation, event emission, trace recording, observability flushing, and state persistence.


1441-1678: LGTM!

The retry loop implementation is comprehensive and handles all edge cases properly:

  • Each retry attempt gets proper observability tracking
  • Suspension and cancellation are detected and handled during both execution and retry delays
  • The waitWithSignal integration ensures delays respect abort signals
  • Error types are distinguished (cancellation vs. suspension vs. retriable errors)
  • The retry count is properly tracked and passed to execution context

2296-2309: LGTM!

The SerializedWorkflowStep interface is properly extended with optional fields for all new step types while maintaining backward compatibility.


2317-2392: LGTM!

The base step serialization properly includes the retries field, and existing step type serialization is preserved.


2394-2507: LGTM!

All new step types are properly serialized with appropriate handling of:

  • Static values vs. functions (serialized via .toString())
  • Nested steps (recursively serialized)
  • Conditional inclusion of optional fields
  • Type-safe discriminators for different variants

The serialization logic is consistent and comprehensive across all new step types.

website/docs/workflows/steps/and-map.md (1)

1-54: LGTM!

The documentation for andMap is comprehensive and clear:

  • Practical Quick Start example demonstrating all major source types
  • Complete function signature with all supported sources
  • Helpful notes clarifying the difference between data, input, and context
website/docs/workflows/steps/and-foreach.md (1)

1-40: LGTM!

The documentation for andForEach is clear and complete:

  • Simple Quick Start example showing array iteration
  • Complete function signature with all parameters
  • Important behavioral notes about array input, order preservation, and concurrency
website/docs/workflows/steps/and-when.md (1)

49-50: Documentation addition looks good.

The retries?: number parameter is correctly positioned in the Function Signature section and logically fits with the other configuration options. Consistent with the broader retry feature additions across the PR.

website/docs/workflows/overview.md (1)

407-428: Well-documented retry feature, but verify for duplication.

The new "Workflow Retry Policies" section is clearly written with a good example demonstrating workflow-level config, per-step override, and runtime override. However, the AI-generated summary indicates this section may appear twice in the file, duplicating the same guidance. Please verify there's no unintended duplication elsewhere in the file that wasn't captured in the provided context.

website/docs/workflows/execute-api.md (1)

12-12: Comprehensive retryCount documentation.

The addition of retryCount to the execute context is well-documented. The Quick Start example correctly includes it in the destructuring, the new dedicated section clearly explains its behavior across both per-step and workflow-level retry scenarios, and the TypeScript interface is correctly updated as an optional property. The examples effectively demonstrate real-world usage patterns.

Also applies to: 150-178, 431-431

.changeset/social-humans-hammer.md (7)

1-3: Changeset format is correct.

The YAML frontmatter properly specifies the package (@voltagent/core) and patch-level version bump for the changelog.


7-20: All imports are consumed.

Each of the 9 imported functions (createWorkflowChain, andThen, andBranch, andForEach, andDoWhile, andDoUntil, andMap, andSleep, andSleepUntil, z) is demonstrated in the examples below. No unused imports.


24-46: Branching example is clear and correct.

The example properly demonstrates conditional branching with multiple branches evaluated against input data, with each branch returning transformed data.


52-79: Verify data type consistency in chained forEach→DoWhile example.

The input is defined as z.array(z.number()) (line 54). After andForEach (lines 56–63), the result should be an array of doubled numbers. However, the subsequent andDoWhile condition (line 70) checks data < 3, treating data as a number rather than an array.

This suggests either a type mismatch in the example or an undocumented data transformation behavior. Please verify that this example accurately reflects the actual API behavior.


84-101: Data shaping example is well-structured.

The andMap example clearly demonstrates composing output from multiple sources (data, step results, context, constant values), with explicit path and source specifications.


106-122: Sleep steps example is correct.

Both andSleep (duration-based) and andSleepUntil (date-based) are demonstrated with proper parameters, chaining naturally into a resuming step.


127-140: Workflow-level retry configuration example is clear.

The example properly shows defining retryConfig at the workflow level and overriding it per-step (with retries: 0), demonstrating flexible retry control.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 18 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/core/src/workflow/core.ts">

<violation number="1" location="packages/core/src/workflow/core.ts:1604">
P2: Duplicate span creation code in retry delay error handling. The same interruptionSpan creation logic is repeated three times for different error types (cancellation, suspension, other). Consider extracting this into a helper function or creating the span once before the error type checks.</violation>
</file>

<file name="website/docs/workflows/steps/and-then.md">

<violation number="1" location="website/docs/workflows/steps/and-then.md:49">
P2: Ambiguous retry documentation: clarify whether 'retries: 2' means 2 retry attempts (3 total) or 2 max attempts (1 retry). Also clarify if retryCount=0 is the initial attempt or first retry.</violation>

<violation number="2" location="website/docs/workflows/steps/and-then.md:60">
P2: Inconsistent retry count explanation: comment says 'increments per retry' but earlier states 0 is the 'first attempt' (not retry). Consider clarifying: 'retryCount=0 for initial attempt, increments by 1 for each retry attempt'.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

4 issues found across 11 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/core/src/workflow/types.ts">

<violation number="1" location="packages/core/src/workflow/types.ts:333">
P1: Type `Error | null` for error field is inconsistent with existing error handling patterns and overly restrictive. Use `unknown` instead to match WorkflowExecutionResult and handle non-Error thrown values.</violation>
</file>

<file name="packages/core/src/workflow/chain.ts">

<violation number="1" location="packages/core/src/workflow/chain.ts:212">
P1: Breaking change: `getStepData` return type now includes nullable `output` field and additional required `status` field. Code expecting `output` to always be present may encounter null reference errors.</violation>
</file>

<file name="website/docs/workflows/overview.md">

<violation number="1" location="website/docs/workflows/overview.md:546">
P2: The description "After each individual step completes" is ambiguous. Based on the detailed hooks documentation, onStepEnd only runs when a step succeeds, not when it fails. Consider clarifying to "After each individual step completes successfully" or "After each individual step succeeds" to match the hooks.md documentation.</violation>
</file>

<file name="packages/core/src/workflow/core.ts">

<violation number="1" location="packages/core/src/workflow/core.ts:1035">
P1: Hook calls in `runTerminalHooks` lack error handling, which can mask original errors or prevent cleanup. Wrap hook invocations in try-catch blocks to ensure workflow state remains consistent even if user hooks fail.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

/**
* Error from the workflow execution, if any
*/
error: Error | null;
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 9, 2026

Choose a reason for hiding this comment

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

P1: Type Error | null for error field is inconsistent with existing error handling patterns and overly restrictive. Use unknown instead to match WorkflowExecutionResult and handle non-Error thrown values.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/src/workflow/types.ts, line 333:

<comment>Type `Error | null` for error field is inconsistent with existing error handling patterns and overly restrictive. Use `unknown` instead to match WorkflowExecutionResult and handle non-Error thrown values.</comment>

<file context>
@@ -297,6 +297,54 @@ export interface WorkflowResumeOptions {
+  /**
+   * Error from the workflow execution, if any
+   */
+  error: Error | null;
+  /**
+   * Suspension metadata when status is suspended
</file context>
Fix with Cubic

data: z.infer<IS>;
state: WorkflowStepState<WorkflowInput<INPUT_SCHEMA>>;
getStepData: (stepId: string) => { input: any; output: any } | undefined;
getStepData: (stepId: string) => WorkflowStepData | undefined;
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 9, 2026

Choose a reason for hiding this comment

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

P1: Breaking change: getStepData return type now includes nullable output field and additional required status field. Code expecting output to always be present may encounter null reference errors.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/src/workflow/chain.ts, line 212:

<comment>Breaking change: `getStepData` return type now includes nullable `output` field and additional required `status` field. Code expecting `output` to always be present may encounter null reference errors.</comment>

<file context>
@@ -208,7 +209,7 @@ export class WorkflowChain<
       data: z.infer<IS>;
       state: WorkflowStepState<WorkflowInput<INPUT_SCHEMA>>;
-      getStepData: (stepId: string) => { input: any; output: any } | undefined;
+      getStepData: (stepId: string) => WorkflowStepData | undefined;
       suspend: (
         reason?: string,
</file context>
Fix with Cubic

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
website/docs/workflows/hooks.md (1)

11-44: Docs update matches the new hook model; fix the missing fenced-code language (MD040).

Proposed fix
-```
+```text
 1. onStart
 ...
 5. onFinish
</details>


Also applies to: 118-138, 209-246

</blockquote></details>
<details>
<summary>packages/core/src/workflow/core.ts (3)</summary><blockquote>

`2288-2339`: **Critical: `executeWithSignalCheck` leaks `setInterval` timers (and listeners) on normal completion.**  
`Promise.race([fn(), abortPromise])` returns when `fn()` resolves, but `abortPromise` keeps the interval running forever.  

<details>
<summary>Proposed fix (ensure cleanup on resolve/reject/abort)</summary>

```diff
 async function executeWithSignalCheck<T>(
   fn: () => Promise<T>,
   signal?: AbortSignal,
   checkInterval = 100, // Check signal every 100ms
 ): Promise<T> {
   if (!signal) {
     // No signal provided, just execute normally
     return await fn();
   }
 
-  // Create a promise that rejects when signal is aborted
-  const abortPromise = new Promise<never>((_, reject) => {
+  let intervalId: ReturnType<typeof setInterval> | undefined;
+  let onAbort: (() => void) | undefined;
+
+  const cleanup = () => {
+    if (intervalId) clearInterval(intervalId);
+    intervalId = undefined;
+    if (onAbort) signal.removeEventListener("abort", onAbort);
+    onAbort = undefined;
+  };
+
+  // Create a promise that rejects when signal is aborted
+  const abortPromise = new Promise<never>((_, reject) => {
     const getAbortError = () => {
       const reason = (signal as AbortSignal & { reason?: unknown }).reason;
       if (reason && typeof reason === "object" && reason !== null && "type" in reason) {
         const typedReason = reason as { type?: string };
         if (typedReason.type === "cancelled") {
           return new Error("WORKFLOW_CANCELLED");
         }
       }
       if (reason === "cancelled") {
         return new Error("WORKFLOW_CANCELLED");
       }
       return new Error("WORKFLOW_SUSPENDED");
     };
 
     const checkSignal = () => {
       if (signal.aborted) {
         reject(getAbortError());
       }
     };
 
     // Check immediately
     checkSignal();
 
     // Set up periodic checking
-    const intervalId = setInterval(checkSignal, checkInterval);
+    intervalId = setInterval(checkSignal, checkInterval);
 
     // Clean up on signal abort
-    signal.addEventListener(
-      "abort",
-      () => {
-        clearInterval(intervalId);
-        reject(getAbortError());
-      },
-      { once: true },
-    );
+    onAbort = () => {
+      cleanup();
+      reject(getAbortError());
+    };
+    signal.addEventListener("abort", onAbort, { once: true });
   });
 
-  // Race between the actual function and abort signal
-  return Promise.race([fn(), abortPromise]);
+  // Race between the actual function and abort signal
+  try {
+    return await Promise.race([fn().finally(cleanup), abortPromise]);
+  } finally {
+    cleanup();
+  }
 }

1185-1307: Major: suspension via pre-step abort signal doesn’t run terminal hooks (onSuspend/onFinish).
In the “signal is aborted and not cancelled” branch, you build suspension state + persist it, but return without calling runTerminalHooks("suspended", { includeEnd: false }). This makes suspension behavior depend on how suspension happens (step-triggered vs externally signaled).


2354-2379: Security/privacy concern: serializing user functions via .toString() can leak source code/secrets.
This now includes more surfaces (sleep/map/loop/branch condition functions). If serializeWorkflowStep is exposed in APIs/UI, consider redacting by default or gating behind an explicit “includeFunctionSource” option.

Also applies to: 2414-2576

🤖 Fix all issues with AI agents
In @packages/core/src/workflow/core.ts:
- Around line 1562-1623: The emitted step-complete event currently hardcodes
status: "success" even when isSkipped is true; update the emitAndCollectEvent
call (the step-complete payload built near emitAndCollectEvent({...}) in the try
block) to set status: isSkipped ? "skipped" : "success" (and keep other fields
like output/result, stepIndex, stepType the same) so the event matches the
stepData.status and span state.
🧹 Nitpick comments (5)
packages/core/src/workflow/hooks.spec.ts (1)

9-13: Avoid reaching into WorkflowRegistry internals via as any for test isolation.

Clearing (registry as any).workflows works, but it’s brittle. Prefer a test-only reset API (e.g., WorkflowRegistry.__resetForTests()), or expose a minimal clear() on the registry.

website/docs/workflows/hooks.md (1)

22-27: Consider printing info.error?.message in examples to avoid noisy object dumps.
(Current snippets interpolate info.error directly.)

Also applies to: 161-167, 202-205

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

1010-1051: Consider isolating hook failures from workflow outcome (especially terminal hooks).
Right now, any thrown error in onSuspend/onError/onFinish/onEnd can flip a completed/suspended/cancelled workflow into the outer catch and be persisted as "error". If hooks are meant to be “observer-only”, wrap each hook with try/catch and log instead.

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

550-662: New chain methods are straightforward and keep data-shape typing readable.
Minor: consider returning ReadonlyArray<...> for andBranch results if you want to signal immutability at the API boundary.

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

202-267: Type surfaces for retry + terminal hooks look coherent and match the new docs/tests.
One nit: clarify in JSDoc whether attempts means “number of retries” vs “total attempts”.

Also applies to: 300-395

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e135ab8 and 3bf64b7.

📒 Files selected for processing (11)
  • .changeset/social-humans-hammer.md
  • packages/core/src/workflow/chain.ts
  • packages/core/src/workflow/context.ts
  • packages/core/src/workflow/core.ts
  • packages/core/src/workflow/hooks.spec.ts
  • packages/core/src/workflow/index.ts
  • packages/core/src/workflow/internal/types.ts
  • packages/core/src/workflow/steps/and-map.spec.ts
  • packages/core/src/workflow/types.ts
  • website/docs/workflows/hooks.md
  • website/docs/workflows/overview.md
✅ Files skipped from review due to trivial changes (1)
  • .changeset/social-humans-hammer.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • website/docs/workflows/overview.md
  • packages/core/src/workflow/steps/and-map.spec.ts
  • packages/core/src/workflow/internal/types.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: Maintain type safety in TypeScript-first codebase
Never use JSON.stringify; use the safeStringify function instead, imported from @voltagent/internal

Files:

  • packages/core/src/workflow/context.ts
  • packages/core/src/workflow/chain.ts
  • packages/core/src/workflow/index.ts
  • packages/core/src/workflow/core.ts
  • packages/core/src/workflow/hooks.spec.ts
  • packages/core/src/workflow/types.ts
🧠 Learnings (1)
📚 Learning: 2026-01-07T05:09:23.217Z
Learnt from: CR
Repo: VoltAgent/voltagent PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-07T05:09:23.217Z
Learning: Applies to **/*.test.ts : Test your changes - ensure all tests pass before committing

Applied to files:

  • packages/core/src/workflow/hooks.spec.ts
🧬 Code graph analysis (1)
packages/core/src/workflow/chain.ts (4)
packages/core/src/workflow/index.ts (2)
  • WorkflowStepData (34-34)
  • andSleep (9-9)
packages/core/src/workflow/types.ts (1)
  • WorkflowStepData (310-315)
packages/core/src/workflow/steps/types.ts (2)
  • WorkflowStepSleepConfig (128-130)
  • WorkflowStepSleepUntilConfig (138-140)
packages/core/src/workflow/steps/and-sleep.ts (1)
  • andSleep (17-31)
🪛 markdownlint-cli2 (0.18.1)
website/docs/workflows/hooks.md

238-238: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: Test core
  • GitHub Check: Test cli
  • GitHub Check: Build (Node 24)
  • GitHub Check: Build (Node 22)
  • GitHub Check: Build (Node 20)
🔇 Additional comments (9)
packages/core/src/workflow/hooks.spec.ts (3)

15-70: Nice coverage of “skipped” snapshots and dual hook surfaces (onFinish vs onEnd).


72-112: Good assertion of error hook ordering and step error snapshot shape.


113-153: Good contract test: suspension triggers onSuspend/onFinish and skips onEnd.

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

1-16: Additive re-exports look fine; please sanity-check for name collisions and missing exports.

Run a quick typecheck/build on the PR branch to ensure ./steps and ./types actually export all listed names (and no duplicate export name conflicts occur).

Also applies to: 23-37

packages/core/src/workflow/context.ts (2)

5-5: Good: stepData now uses the shared WorkflowStepData type.

Also applies to: 57-57


92-105: Verify stepType literal strings match actual runtime step.type values everywhere.
A mismatch here tends to silently break tracing/UI filters.

Use a quick search/typecheck to confirm the canonical set of step type strings is consistent across packages/core/src/workflow/steps/*, packages/core/src/utils/node-utils.ts, and stream/history typing.

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

22-22: Retry delay uses waitWithSignal—good choice (abort-aware + no JSON.stringify).

Also applies to: 1660-1734

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

199-370: Good: getStepData now reflects richer step snapshots (status/error), improving type safety.

Also applies to: 377-470, 477-548

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

584-609: Good: step type unions updated across history and streaming to include new step kinds.

Also applies to: 684-753

omeraplak and others added 4 commits January 9, 2026 13:52
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
…t/voltagent into feat/add-workflow-control-steps
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (5)
packages/core/src/workflow/steps/and-guardrail.spec.ts (1)

31-53: Consider adding edge case coverage.

While the current tests cover the happy paths, consider adding tests for:

  • Guardrails that block execution (pass: false)
  • Multiple guardrails applied in sequence
  • Error handling when guardrail handler throws

These would provide more comprehensive coverage of the andGuardrail behavior.

Based on learnings, ensure all tests pass before committing.

examples/with-workflow/src/index.ts (1)

535-551: Clarify the guardrail application order.

The andGuardrail step at line 544-547 applies outputGuardrails: [redactNumbers], but the workflow also has outputGuardrails: [redactNumbers] at the workflow level (line 542). This means numbers will be redacted twice (once by the step guardrail, once by the workflow guardrail), which for this particular guardrail is idempotent but could be confusing as an example.

Consider either removing the duplicate or adding a comment explaining the intentional double-application for demonstration purposes.

packages/core/src/workflow/internal/guardrails.ts (1)

106-107: Consider strengthening the type guard validation.

The type guard checks if value is a string or array, but doesn't validate that arrays contain valid message types (UIMessage[] or BaseMessage[]). This could allow invalid array contents through at runtime.

However, since this is a type guard primarily for TypeScript narrowing and the actual guardrail functions will validate the content, this may be acceptable for the current use case.

♻️ Optional: Add runtime validation for array contents
-export const isWorkflowGuardrailInput = (value: unknown): value is WorkflowGuardrailInput =>
-  typeof value === "string" || Array.isArray(value);
+export const isWorkflowGuardrailInput = (value: unknown): value is WorkflowGuardrailInput => {
+  if (typeof value === "string") return true;
+  if (!Array.isArray(value)) return false;
+  // Arrays are accepted; content validation is deferred to guardrail execution
+  return true;
+};
packages/core/src/workflow/core.ts (1)

2628-2648: Verify mapConfig serialization size.

The map config serialization uses safeStringify which is good. However, if the map contains many entries with large function bodies, the serialized output could become quite large. Consider if there should be any size limits or truncation for very large configurations.

packages/core/src/workflow/steps/types.ts (1)

259-259: Note: WorkflowStepForEach uses any for ITEM and RESULT generics in the union.

The WorkflowStepForEach<INPUT, any, any> uses any for the ITEM and RESULT type parameters in the union. This is a pragmatic choice to avoid complex variance issues in the union type, but it does weaken type safety at the union level.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 488fa8c and 6967cfd.

📒 Files selected for processing (22)
  • .changeset/social-humans-hammer.md
  • examples/with-workflow/src/index.ts
  • packages/core/src/agent/guardrail.ts
  • packages/core/src/agent/types.ts
  • packages/core/src/index.ts
  • packages/core/src/workflow/chain.ts
  • packages/core/src/workflow/context.ts
  • packages/core/src/workflow/core.ts
  • packages/core/src/workflow/guardrails.spec.ts
  • packages/core/src/workflow/index.ts
  • packages/core/src/workflow/internal/guardrails.ts
  • packages/core/src/workflow/open-telemetry/trace-context.ts
  • packages/core/src/workflow/steps/and-guardrail.spec.ts
  • packages/core/src/workflow/steps/and-guardrail.ts
  • packages/core/src/workflow/steps/helpers.ts
  • packages/core/src/workflow/steps/index.ts
  • packages/core/src/workflow/steps/types.ts
  • packages/core/src/workflow/types.ts
  • website/docs/workflows/overview.md
  • website/docs/workflows/steps/and-all.md
  • website/docs/workflows/steps/and-guardrail.md
  • website/sidebars.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/core/src/index.ts
  • website/sidebars.ts
  • .changeset/social-humans-hammer.md
  • packages/core/src/workflow/steps/helpers.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: Maintain type safety in TypeScript-first codebase
Never use JSON.stringify; use the safeStringify function instead, imported from @voltagent/internal

Files:

  • packages/core/src/agent/types.ts
  • examples/with-workflow/src/index.ts
  • packages/core/src/workflow/steps/and-guardrail.ts
  • packages/core/src/agent/guardrail.ts
  • packages/core/src/workflow/index.ts
  • packages/core/src/workflow/open-telemetry/trace-context.ts
  • packages/core/src/workflow/steps/and-guardrail.spec.ts
  • packages/core/src/workflow/guardrails.spec.ts
  • packages/core/src/workflow/context.ts
  • packages/core/src/workflow/steps/index.ts
  • packages/core/src/workflow/internal/guardrails.ts
  • packages/core/src/workflow/core.ts
  • packages/core/src/workflow/chain.ts
  • packages/core/src/workflow/types.ts
  • packages/core/src/workflow/steps/types.ts
🧠 Learnings (1)
📚 Learning: 2026-01-07T05:09:23.217Z
Learnt from: CR
Repo: VoltAgent/voltagent PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-07T05:09:23.217Z
Learning: Applies to **/*.test.ts : Test your changes - ensure all tests pass before committing

Applied to files:

  • packages/core/src/workflow/steps/and-guardrail.spec.ts
  • packages/core/src/workflow/guardrails.spec.ts
🧬 Code graph analysis (5)
examples/with-workflow/src/index.ts (6)
packages/core/src/index.ts (4)
  • createWorkflowChain (3-3)
  • andThen (6-6)
  • createInputGuardrail (78-78)
  • createOutputGuardrail (78-78)
packages/core/src/workflow/chain.ts (2)
  • createWorkflowChain (919-932)
  • andThen (366-372)
packages/core/src/workflow/index.ts (2)
  • createWorkflowChain (20-20)
  • andThen (3-3)
packages/core/src/workflow/steps/index.ts (1)
  • andThen (2-2)
packages/core/src/workflow/steps/and-then.ts (1)
  • andThen (31-85)
packages/core/src/agent/guardrail.ts (2)
  • createInputGuardrail (62-72)
  • createOutputGuardrail (74-89)
packages/core/src/workflow/steps/and-guardrail.spec.ts (4)
packages/core/src/agent/guardrail.ts (2)
  • createOutputGuardrail (74-89)
  • createInputGuardrail (62-72)
packages/core/src/workflow/chain.ts (1)
  • andGuardrail (555-560)
packages/core/src/workflow/steps/and-guardrail.ts (1)
  • andGuardrail (16-84)
packages/core/src/test-utils/mocks/workflows.ts (1)
  • createMockWorkflowExecuteContext (19-41)
packages/core/src/workflow/context.ts (3)
packages/core/src/workflow/index.ts (1)
  • WorkflowStepData (35-35)
packages/core/src/workflow/types.ts (1)
  • WorkflowStepData (323-328)
packages/core/src/index.ts (1)
  • Agent (34-34)
packages/core/src/workflow/chain.ts (7)
packages/core/src/workflow/types.ts (2)
  • WorkflowStepData (323-328)
  • WorkflowInput (410-415)
packages/core/src/workflow/steps/and-guardrail.ts (1)
  • andGuardrail (16-84)
packages/core/src/workflow/steps/and-sleep.ts (1)
  • andSleep (17-31)
packages/core/src/workflow/steps/and-sleep-until.ts (1)
  • andSleepUntil (17-37)
packages/core/src/workflow/steps/and-branch.ts (1)
  • andBranch (10-91)
packages/core/src/workflow/steps/and-foreach.ts (1)
  • andForEach (10-112)
packages/core/src/workflow/steps/and-map.ts (1)
  • andMap (79-101)
packages/core/src/workflow/steps/types.ts (4)
packages/core/src/workflow/internal/types.ts (4)
  • InternalWorkflowStepConfig (69-87)
  • InternalBaseWorkflowStep (93-138)
  • InternalWorkflowFunc (65-67)
  • InternalAnyWorkflowStep (144-152)
packages/core/src/agent/types.ts (2)
  • InputGuardrail (404-404)
  • OutputGuardrail (418-420)
packages/core/src/index.ts (3)
  • InputGuardrail (176-176)
  • OutputGuardrail (177-177)
  • Workflow (26-26)
packages/core/src/workflow/index.ts (1)
  • Workflow (26-26)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Test core
  • GitHub Check: Build (Node 24)
  • GitHub Check: Build (Node 22)
  • GitHub Check: Build (Node 20)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (49)
website/docs/workflows/overview.md (4)

407-429: LGTM! Clear documentation of retry policies.

The retry policy section provides a clear, concise example showing workflow-level defaults and step-level overrides. The documentation correctly explains the inheritance behavior and opt-out mechanism.


430-478: LGTM! Comprehensive guardrails documentation.

The guardrails section provides excellent examples and clearly documents the constraint that input guardrails only accept string or message inputs. The code examples demonstrate both workflow-level and step-level guardrail usage effectively.


592-600: LGTM! Hooks table properly expanded.

The workflow hooks table has been updated with new lifecycle hooks (onSuspend, onError, onFinish) and improved descriptions. This aligns well with the PR's expansion of workflow lifecycle management.


407-478: No action needed — sections are not duplicated.

The search confirms that both "Workflow Retry Policies" (line 407) and "Workflow Guardrails" (line 430) appear only once in the file. The AI summary indicating duplicates was inaccurate.

packages/core/src/workflow/steps/and-guardrail.spec.ts (1)

1-54: LGTM! Clean test coverage for andGuardrail.

The test suite covers the core functionality:

  • Output guardrails successfully modify data
  • Input guardrails successfully modify string data

The tests are well-structured and use appropriate mocking utilities.

packages/core/src/workflow/open-telemetry/trace-context.ts (2)

256-284: LGTM! Well-designed generic child span method.

The createChildSpan method provides flexible span creation with:

  • Proper attribute inheritance via commonAttributes
  • Optional parent span support
  • Configurable span kind and attributes

The implementation follows existing patterns in the class and correctly uses safeStringify per coding guidelines.


356-362: LGTM! Useful method for setting input after span creation.

The setInput method allows setting input on the root span after initialization, which is useful for scenarios where input isn't available at construction time (e.g., guardrail modifications). Correctly uses safeStringify per coding guidelines.

packages/core/src/agent/types.ts (1)

491-496: LGTM! Appropriate type extension for workflow evaluation.

Adding "workflow" to the AgentEvalOperationType union enables evaluation support for workflow operations, aligning with the broader workflow feature additions in this PR. The change is purely additive and non-breaking.

packages/core/src/workflow/guardrails.spec.ts (1)

1-86: LGTM! Comprehensive guardrail integration tests.

The test suite provides excellent coverage:

  • Input guardrails modify data before execution (trim example)
  • Output guardrails modify data after execution (redact example)
  • Blocking guardrails throw with proper error code

The tests properly use beforeEach for registry cleanup, ensuring test isolation. Error assertions use toMatchObject to verify the specific error code, which is a good practice.

Based on learnings, ensure all tests pass before committing.

packages/core/src/workflow/index.ts (2)

1-17: LGTM!

The new workflow step combinators (andGuardrail, andSleep, andSleepUntil, andForEach, andBranch, andDoWhile, andDoUntil, andMap) are properly re-exported from the steps module, expanding the public API surface appropriately.


24-38: LGTM!

The new type exports (WorkflowHookContext, WorkflowHookStatus, WorkflowRetryConfig, WorkflowStepData, WorkflowStepStatus) are correctly added to the public API, aligning with the PR's lifecycle hook and retry policy enhancements.

packages/core/src/agent/guardrail.ts (1)

165-186: LGTM!

The addition of the generic type parameter <TOutput = any> to normalizeOutputGuardrailList improves type safety for consumers while maintaining backward compatibility through the default any type. The internal normalization correctly continues to use <any> for the return type.

examples/with-workflow/src/index.ts (5)

2-10: LGTM!

The expanded imports correctly bring in the new workflow helpers (andGuardrail, andThen, createInputGuardrail, createOutputGuardrail) needed for the new example workflows.


368-401: LGTM!

The Timed Reminder Workflow is a clean example demonstrating andSleep and andSleepUntil. Good defensive programming with Math.max(0, data.waitMs) to prevent negative durations.


403-441: LGTM!

The Batch Transform Workflow effectively demonstrates andForEach with concurrency control and andMap for composing outputs from multiple sources. The use of source types ("input", "data", "fn") provides a clear example of the mapping capabilities.


443-511: LGTM!

The Loop + Branch Workflow correctly demonstrates andDoWhile, andDoUntil, and andBranch. The select-branch step properly handles the array output from andBranch (which runs all matching branches and returns aligned results).


573-576: LGTM!

The new workflows are correctly registered with the VoltAgent configuration.

website/docs/workflows/steps/and-all.md (1)

65-100: LGTM!

The documentation for the new retries option is comprehensive. The example clearly demonstrates how retryCount works, and the behavioral rules are well-documented (retry on errors only, not suspend/cancel; override semantics; scoped behavior across parallel steps).

website/docs/workflows/steps/and-guardrail.md (1)

1-112: LGTM!

The documentation for andGuardrail is well-structured with clear examples covering:

  • Basic output guardrail usage
  • Input guardrails for string/message data
  • Output guardrails for structured data
  • The distinction between when to use input vs output guardrails

The note about guardrailAgent for guardrails needing agent APIs is helpful.

packages/core/src/workflow/steps/and-guardrail.ts (3)

16-26: LGTM!

Good design choice to normalize guardrail lists at step creation time rather than execution time, improving runtime efficiency. The generic type parameters properly flow through to the execute context.


33-55: LGTM!

The early return for empty guardrails is efficient. The guardrail runtime is properly constructed with all necessary context including workflow IDs, trace context, logger, and the optional guardrailAgent for guardrails that need agent APIs.


59-79: LGTM!

The input guardrail type check at line 60 with a descriptive error message provides clear guidance when users misconfigure guardrails. The sequential application of input then output guardrails follows the documented behavior.

packages/core/src/workflow/context.ts (4)

3-6: LGTM!

The new imports for Agent and WorkflowStepData types support the expanded context properties added in this PR.


58-58: LGTM!

The stepData type update from an inline type to WorkflowStepData is an improvement, as WorkflowStepData includes additional fields (status, error) that provide richer step execution tracking.


79-82: LGTM!

The optional guardrailAgent property enables workflows to supply an agent instance for guardrails that require agent APIs or metadata, aligning with the new andGuardrail step functionality.


97-111: LGTM!

The expanded stepType union comprehensively covers all the new step types introduced in this PR: "tap", "workflow", "guardrail", "sleep", "sleep-until", "foreach", "loop", "branch", and "map".

packages/core/src/workflow/steps/index.ts (2)

7-13: LGTM!

The new step exports are well-organized and follow the existing pattern. All new workflow control step creators (andGuardrail, andSleep, andSleepUntil, andForEach, andBranch, andDoWhile, andDoUntil, andMap) are properly exported for public consumption.


26-36: LGTM!

The type exports are comprehensive and properly aligned with the new step creators. All configuration and result types are correctly exported.

packages/core/src/workflow/internal/guardrails.ts (3)

83-104: LGTM! Good use of Proxy for the stub agent.

The Proxy pattern provides clear, actionable error messaging when users attempt to call unsupported agent methods, guiding them to provide a guardrailAgent in the workflow config or run options.


147-195: LGTM!

The createWorkflowGuardrailRuntime function properly assembles the operation context with all required fields including trace context, logger, and abort controller. The deterministic operationId generation is a good pattern for debugging and tracing.


197-235: LGTM!

The applyWorkflowInputGuardrails and applyWorkflowOutputGuardrails functions properly short-circuit when no guardrails are configured and correctly delegate to the underlying guardrail runners.

packages/core/src/workflow/chain.ts (4)

552-560: LGTM!

The andGuardrail method properly follows the existing fluent API pattern and returns this since guardrails don't transform the data type.


562-580: LGTM!

The andSleep and andSleepUntil methods correctly preserve the current data type since sleep operations don't modify the workflow data.


582-618: LGTM!

The andBranch and andForEach methods properly transform the data type - andBranch returns Array<NEW_DATA | undefined> and andForEach returns NEW_DATA[], which correctly reflects the semantics of these operations.


620-674: LGTM!

The andDoWhile, andDoUntil, and andMap methods follow the established pattern. The loop methods return NEW_DATA (the result of the last iteration), and andMap returns WorkflowStepMapResult<MAP> as expected.

packages/core/src/workflow/core.ts (6)

1051-1057: LGTM!

The retry configuration normalization properly handles edge cases with Number.isFinite checks and Math.max(0, ...) to ensure non-negative values.


1059-1092: LGTM!

The buildHookContext and runTerminalHooks helpers centralize terminal hook invocation logic. The conditional logic for onEnd (excluding it for suspended status by default) is sensible since suspended workflows may resume.


1552-1568: Verify step data status updates within retry loop.

The step data status is reset to "running" at the start of each retry attempt (lines 1554-1558), which is correct. However, ensure that this doesn't conflict with any external observers that might read the step data during execution.


1704-1720: LGTM!

The retry logging includes the current attempt count and limit, which is valuable for debugging. The error details are properly captured.


1721-1793: LGTM! Retry delay properly handles signals.

The retry delay using waitWithSignal correctly handles both cancellation and suspension signals during the delay period, preventing the workflow from continuing retries if a signal is received.


2546-2648: LGTM!

The serialization logic for new step types (sleep, sleep-until, foreach, loop, branch, map, guardrail) properly handles both static values and function references, converting functions to strings for serialization.

packages/core/src/workflow/types.ts (4)

203-214: LGTM!

The WorkflowRetryConfig interface is well-documented with clear JSDoc comments indicating the default values. The optional nature of both fields provides flexibility.


313-359: LGTM!

The new hook-related types provide a comprehensive view of workflow state at terminal points:

  • WorkflowHookStatus covers all terminal states
  • WorkflowStepStatus includes all possible step statuses including "skipped"
  • WorkflowStepData captures input, output, status, and error for each step
  • WorkflowHookContext aggregates all this information for hook consumers

381-407: LGTM!

The expanded WorkflowHooks interface with onSuspend, onError, and onFinish provides more granular control over lifecycle events. The updated onEnd signature accepting an optional WorkflowHookContext maintains backward compatibility while enabling richer debugging.


627-641: LGTM!

The extended stepType union in BaseWorkflowStepHistoryEntry correctly includes all new step types for proper history tracking.

packages/core/src/workflow/steps/types.ts (4)

62-72: LGTM!

The guardrail step types properly reuse the existing InputGuardrail and OutputGuardrail types from the agent module, maintaining consistency across the codebase.


142-160: LGTM!

The sleep step types properly support both static values and dynamic functions for duration/date, providing flexibility for time-based workflow control.


203-238: LGTM!

The map step types are well-designed:

  • WorkflowStepMapEntry provides a comprehensive union of data sources (value, data, input, step, context, fn)
  • WorkflowStepMapEntryResult uses conditional types to infer the result type
  • WorkflowStepMapResult maps over the configuration to produce the output type

267-279: No issues found. The signature change from InternalExtractWorkflowInputData<DATA> to DATA is compatible with all existing callers. Since InternalExtractWorkflowInputData<T> resolves to T when T is a concrete type, the two signatures are functionally equivalent for actual usage. All callers already pass DATA values directly to workflow.run(), and no breaking changes exist.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 11 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/core/src/workflow/types.ts">

<violation number="1" location="packages/core/src/workflow/types.ts:325">
P2: Breaking change: Changed `output` from required field with null union (`output: Type | null`) to optional field (`output?: Type`). This is a breaking change because code checking `output === null` will now receive `undefined`, and code expecting the field to always be present may fail. While this aligns with implementation usage, it may break existing consumers.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.


export type WorkflowStepData = {
input: DangerouslyAllowAny;
output?: DangerouslyAllowAny;
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 9, 2026

Choose a reason for hiding this comment

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

P2: Breaking change: Changed output from required field with null union (output: Type | null) to optional field (output?: Type). This is a breaking change because code checking output === null will now receive undefined, and code expecting the field to always be present may fail. While this aligns with implementation usage, it may break existing consumers.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/src/workflow/types.ts, line 325:

<comment>Breaking change: Changed `output` from required field with null union (`output: Type | null`) to optional field (`output?: Type`). This is a breaking change because code checking `output === null` will now receive `undefined`, and code expecting the field to always be present may fail. While this aligns with implementation usage, it may break existing consumers.</comment>

<file context>
@@ -322,7 +322,7 @@ export type WorkflowStepStatus =
 export type WorkflowStepData = {
   input: DangerouslyAllowAny;
-  output: DangerouslyAllowAny | null;
+  output?: DangerouslyAllowAny;
   status: WorkflowStepStatus;
   error?: Error | null;
</file context>
Fix with Cubic

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 @.changeset/social-humans-hammer.md:
- Around line 131-145: The example workflow references data.userId but never
defines the workflow input, so update the createWorkflowChain call to declare an
input schema (e.g., input: z.object({ userId: z.string() })) or modify the
example to use a clearly defined prior step output; specifically edit the
createWorkflowChain invocation to include an input definition and ensure the
"fetch-user" step (id "fetch-user") reads from that input (data.userId), and
keep the "no-retry-step" behavior unchanged.
🧹 Nitpick comments (10)
packages/core/src/workflow/steps/and-foreach.spec.ts (3)

61-82: Potential flakiness: timer-based ordering assertions

setTimeout-driven delays can be noisy under CI load; consider making completion order deterministic without relying on wall-clock timing (e.g., deferred promises you resolve in a chosen order), while still exercising concurrency.


84-110: Concurrency test can false-pass for serial execution; also ensure decrement on failure

expect(maxInFlight).toBeLessThanOrEqual(2) would still pass if the implementation is fully serial (maxInFlight === 1). Also, inFlight -= 1 should be in a finally to avoid leaking state if anything throws.

Proposed tightening
-        execute: async ({ data }) => {
-          inFlight += 1;
-          maxInFlight = Math.max(maxInFlight, inFlight);
-          await new Promise((resolve) => setTimeout(resolve, 20));
-          inFlight -= 1;
-          return data;
-        },
+        execute: async ({ data }) => {
+          inFlight += 1;
+          maxInFlight = Math.max(maxInFlight, inFlight);
+          try {
+            await new Promise((resolve) => setTimeout(resolve, 20));
+            return data;
+          } finally {
+            inFlight -= 1;
+          }
+        },
       }),
     });

     await step.execute(
       createMockWorkflowExecuteContext({
         data: [1, 2, 3, 4],
       }),
     );

     expect(maxInFlight).toBeLessThanOrEqual(2);
+    // Guards against a fully-serial implementation “passing” this test.
+    expect(maxInFlight).toBeGreaterThan(1);

112-128: Avoid as any in TS tests; prefer @ts-expect-error / unknown and loosen message matching

Using as any sidesteps type-safety unnecessarily; also, matching the full error string can be brittle.

Proposed adjustment
     await expect(
       step.execute(
         createMockWorkflowExecuteContext({
-          data: { value: 1 } as any,
+          // @ts-expect-error - intentionally invalid input
+          data: { value: 1 },
         }),
       ),
-    ).rejects.toThrow("andForEach expects array input data");
+    ).rejects.toThrow(/andForEach expects array input data/);
packages/core/src/workflow/steps/and-map.spec.ts (2)

6-39: Avoid as any in the mock state to keep tests resilient to type changes.

The cast on Line 24 can hide breaking changes in WorkflowExecuteContext / state shape; prefer updating createMockWorkflowExecuteContext to accept a typed state (or expose a helper to build it).


41-62: Use Vitest fake timers to avoid flakiness from real setTimeout.

Line 48 introduces a timing dependency; fake timers (or vi.waitFor) makes this deterministic and faster.

.changeset/social-humans-hammer.md (1)

149-170: Consider clarifying hook param shapes in the snippet (esp. info.steps).

The example assumes info.steps is an object (Line 165); if it’s a Map or different structure, this will mislead—maybe add a short comment or align to the exported type shape.

packages/core/src/workflow/steps/and-map.ts (3)

10-27: Validate path segments to avoid surprising lookups ("", leading/trailing dots).

As-is, readPath(value, "") will access value[""] (Line 15-20). If that’s not intended, reject empty segments early.

Proposed fix
 const readPath = (value: unknown, path?: string) => {
   if (path === undefined || path === ".") {
     return value;
   }
 
-  const parts = path.split(".");
+  const parts = path.split(".");
+  if (parts.some((p) => p.length === 0)) {
+    throw new Error(`Invalid path '${path}'`);
+  }
   let current: any = value;
 
   for (const part of parts) {
     if (current && typeof current === "object") {
       current = current[part];
     } else {
       throw new Error(`Invalid path '${path}'`);
     }
   }
 
   return current;
 };

46-74: Improve error detail for unsupported sources (easier debugging).

Line 72 throws a generic error; including entry.source (or using an assertNever) makes misconfigurations easier to diagnose.


79-101: Consider resolving map entries concurrently (optional perf win).

Current loop (Line 91-96) awaits sequentially; for many independent entries (esp. multiple fn sources), Promise.all can reduce latency while keeping a stable output shape.

Possible refactor
     execute: async (context) => {
       const entries = Object.entries(map) as Array<[keyof MAP, MAP[keyof MAP]]>;
       const result = {} as WorkflowStepMapResult<MAP>;
 
-      for (const [key, entry] of entries) {
-        result[key] = (await resolveMapEntry(
-          entry,
-          context,
-        )) as WorkflowStepMapResult<MAP>[typeof key];
-      }
+      await Promise.all(
+        entries.map(async ([key, entry]) => {
+          result[key] = (await resolveMapEntry(
+            entry,
+            context,
+          )) as WorkflowStepMapResult<MAP>[typeof key];
+        }),
+      );
 
       return result;
     },
packages/core/src/workflow/core.ts (1)

1568-1790: Consider extracting retry logic for clarity

The retry loop implementation is functionally correct but has high cognitive complexity:

  • Infinite while (true) loop with internal break
  • Nested error handling within retry delay (lines 1737-1779)
  • Multiple interleaved concerns (execution, retry, cancellation, suspension)
♻️ Optional refactor to improve maintainability

Consider extracting the retry loop into a separate function:

async function executeStepWithRetries<T>(
  stepExecutor: () => Promise<T>,
  options: {
    stepRetryLimit: number;
    retryDelayMs: number;
    signal?: AbortSignal;
    onRetry?: (retryCount: number) => void;
  }
): Promise<T> {
  let retryCount = 0;
  let lastError: unknown;
  
  while (retryCount <= options.stepRetryLimit) {
    try {
      return await stepExecutor();
    } catch (error) {
      lastError = error;
      
      if (shouldPropagateError(error) || retryCount >= options.stepRetryLimit) {
        throw error;
      }
      
      retryCount++;
      options.onRetry?.(retryCount);
      
      if (options.retryDelayMs > 0) {
        await waitWithSignal(options.retryDelayMs, options.signal);
      }
    }
  }
  
  throw lastError;
}

This would make the main execution loop cleaner and the retry logic more testable.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6967cfd and 120c5a7.

📒 Files selected for processing (11)
  • .changeset/social-humans-hammer.md
  • packages/core/src/workflow/core.ts
  • packages/core/src/workflow/hooks.spec.ts
  • packages/core/src/workflow/steps/and-branch.spec.ts
  • packages/core/src/workflow/steps/and-foreach.spec.ts
  • packages/core/src/workflow/steps/and-map.spec.ts
  • packages/core/src/workflow/steps/and-map.ts
  • packages/core/src/workflow/steps/and-sleep.spec.ts
  • packages/core/src/workflow/types.ts
  • website/docs/workflows/steps/and-branch.md
  • website/docs/workflows/steps/and-then.md
✅ Files skipped from review due to trivial changes (1)
  • website/docs/workflows/steps/and-branch.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/core/src/workflow/steps/and-sleep.spec.ts
  • packages/core/src/workflow/hooks.spec.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: Maintain type safety in TypeScript-first codebase
Never use JSON.stringify; use the safeStringify function instead, imported from @voltagent/internal

Files:

  • packages/core/src/workflow/steps/and-map.ts
  • packages/core/src/workflow/steps/and-foreach.spec.ts
  • packages/core/src/workflow/steps/and-branch.spec.ts
  • packages/core/src/workflow/steps/and-map.spec.ts
  • packages/core/src/workflow/types.ts
  • packages/core/src/workflow/core.ts
🧠 Learnings (1)
📚 Learning: 2026-01-07T05:09:23.217Z
Learnt from: CR
Repo: VoltAgent/voltagent PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-07T05:09:23.217Z
Learning: Applies to **/*.test.ts : Test your changes - ensure all tests pass before committing

Applied to files:

  • packages/core/src/workflow/steps/and-foreach.spec.ts
  • packages/core/src/workflow/steps/and-branch.spec.ts
  • packages/core/src/workflow/steps/and-map.spec.ts
🧬 Code graph analysis (2)
packages/core/src/workflow/steps/and-branch.spec.ts (2)
packages/core/src/workflow/steps/and-branch.ts (1)
  • andBranch (10-91)
packages/core/src/test-utils/mocks/workflows.ts (1)
  • createMockWorkflowExecuteContext (19-41)
packages/core/src/workflow/types.ts (3)
packages/core/src/workflow/index.ts (5)
  • WorkflowRetryConfig (29-29)
  • WorkflowHookStatus (28-28)
  • WorkflowStepStatus (36-36)
  • WorkflowStepData (35-35)
  • WorkflowHookContext (27-27)
packages/core/src/index.ts (4)
  • InputGuardrail (176-176)
  • OutputGuardrail (177-177)
  • Agent (34-34)
  • context (111-111)
packages/internal/src/types/index.ts (1)
  • DangerouslyAllowAny (6-6)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: Test core
  • GitHub Check: Test cli
  • GitHub Check: Build (Node 20)
  • GitHub Check: Build (Node 22)
  • GitHub Check: Build (Node 24)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (23)
packages/core/src/workflow/steps/and-foreach.spec.ts (3)

1-6: Solid baseline coverage + readable structure

Imports and the overall spec layout are clean and focused on behavior.


7-59: Core behavior tests look good (map/empty/single)

These cover the main “happy path” and key edge cases without over-specifying internals.


130-148: Error propagation coverage is good

This is the right level of assertion to ensure inner step failures surface properly.

website/docs/workflows/steps/and-then.md (2)

45-50: LGTM: Clear parameter documentation

The retryCount parameter is well-documented, with a clear explanation of its zero-based indexing for the initial attempt.


53-72: LGTM: Comprehensive retry documentation

The Retries section clearly explains:

  • Retry behavior applies only to thrown errors
  • How retryCount increments across attempts
  • Relationship between per-step retries and workflow-wide retryConfig

This provides users with a complete understanding of the retry mechanism.

packages/core/src/workflow/steps/and-branch.spec.ts (6)

7-42: LGTM: Comprehensive test for branch index alignment

This test correctly verifies that andBranch maintains result array alignment with branch definition order, returning undefined for non-matching branches. The test data (value: 5) properly exercises multiple conditions.


44-72: LGTM: Clear test for non-matching branches

Properly validates that non-matching branches return undefined while maintaining array structure.


74-87: LGTM: Good edge case coverage

This test properly validates the behavior when no branches are provided.


89-112: LGTM: Proper error propagation test

Correctly verifies that errors thrown within branch steps are propagated to the caller.


114-139: LGTM: Cancellation handling test

Properly validates that andBranch respects workflow cancellation signals and throws the expected error.


141-166: LGTM: Suspension handling test

Correctly validates that andBranch respects workflow suspension signals.

packages/core/src/workflow/core.ts (7)

968-996: LGTM: Well-structured guardrail integration

The guardrail runtime setup is clean:

  • Resolves guardrail sets from multiple sources
  • Conditionally creates runtime only when needed
  • Properly integrates guardrailAgent into execution context

This follows a sensible pattern for optional feature integration.


1051-1058: LGTM: Robust retry configuration handling

The retry config calculation properly:

  • Falls back to workflow-level defaults
  • Validates numeric inputs with Number.isFinite
  • Ensures non-negative integers with Math.max and Math.floor

This prevents invalid retry configurations.


1059-1109: LGTM: Well-designed terminal hook orchestration

The runTerminalHooks helper provides:

  • Consistent hook context building via buildHookContext
  • Safe hook execution with individual try/catch blocks
  • Proper conditional logic for onEnd (excludes suspended unless explicitly requested)
  • Step data snapshots in the hook context

This is a clean abstraction that reduces duplication across terminal states.


1111-1129: LGTM: Proper guardrail application placement

Input and output guardrails are correctly applied:

  • Input guardrails: before workflow execution with validation
  • Output guardrails: after all steps complete
  • State properly updated with guardrailed data

The error thrown for invalid input structure (line 1113-1116) provides clear guidance to users.

Also applies to: 1793-1805


1425-1430: LGTM: Complete step lifecycle tracking

Step data tracking properly maintains input/output/status/error throughout the step lifecycle:

  • Initialization with running status
  • Reset on retry attempts
  • Success/skipped/error status updates
  • Used in hook contexts to provide step snapshots

This enables detailed workflow execution introspection.


2532-2661: LGTM: Comprehensive step type serialization

The serialization logic for new step types (sleep, sleep-until, foreach, loop, branch, map, guardrail, workflow) is well-structured:

  • Extracts type-specific configuration
  • Recursively serializes nested steps
  • Converts functions to strings for inspection

This enables complete workflow introspection via getFullState.


2352-2407: LGTM: Elegant signal checking implementation

The executeWithSignalCheck helper provides responsive suspension:

  • Periodic signal polling (configurable interval)
  • Proper differentiation between cancellation and suspension
  • Promise.race pattern for clean async flow
  • Cleanup of interval on abort

This enables "immediate" suspension mode while maintaining clean async code.

packages/core/src/workflow/types.ts (5)

203-214: LGTM: Clear retry configuration interface

WorkflowRetryConfig provides a simple, well-documented structure for retry settings with sensible defaults (0 retries, 0 delay).


264-280: LGTM: Comprehensive run options extension

The additions to WorkflowRunOptions properly support:

  • Retry configuration override
  • Input/output guardrail specification
  • Custom guardrail agent injection

All fields are appropriately optional with clear documentation.


313-359: LGTM: Well-designed hook context types

The new hook types provide a robust foundation:

  • WorkflowHookStatus: terminal states for workflow completion
  • WorkflowStepStatus: comprehensive step lifecycle states
  • WorkflowStepData: per-step execution details
  • WorkflowHookContext: unified context for terminal hooks with status, state, result, error, suspension, cancellation, and step snapshots

These types enable rich hook implementations with complete execution context.


627-641: LGTM: Consistent step type taxonomy

The step type enumerations are consistently extended across:

  • BaseWorkflowStepHistoryEntry.stepType
  • WorkflowStreamEvent.stepType

All new step types (tap, workflow, guardrail, sleep, sleep-until, foreach, loop, branch, map) are properly included.

Also applies to: 773-787


381-407: Hook signatures documented in changeset—no action required.

The breaking changes to hook signatures (onSuspend, onError, onFinish now receive WorkflowHookContext; onEnd receives both state and optional context) are already documented in .changeset/social-humans-hammer.md with working code examples. This changeset will generate the CHANGELOG entry upon release.

Comment on lines +131 to +145
```ts
createWorkflowChain({
id: "retry-defaults",
retryConfig: { attempts: 2, delayMs: 500 },
})
.andThen({
id: "fetch-user",
execute: async ({ data }) => fetchUser(data.userId),
})
.andThen({
id: "no-retry-step",
retries: 0,
execute: async ({ data }) => data,
});
```
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Retry example uses data.userId without defining where userId comes from.

Lines 132-139 read data.userId, but the workflow definition doesn’t declare input (or a prior step) producing it—worth adjusting to avoid confusing docs (e.g., add input: z.object({ userId: z.string() }) or change the call to use whatever input you define).

🤖 Prompt for AI Agents
In @.changeset/social-humans-hammer.md around lines 131 - 145, The example
workflow references data.userId but never defines the workflow input, so update
the createWorkflowChain call to declare an input schema (e.g., input: z.object({
userId: z.string() })) or modify the example to use a clearly defined prior step
output; specifically edit the createWorkflowChain invocation to include an input
definition and ensure the "fetch-user" step (id "fetch-user") reads from that
input (data.userId), and keep the "no-retry-step" behavior unchanged.

@omeraplak omeraplak merged commit 78ff377 into main Jan 9, 2026
23 checks passed
@omeraplak omeraplak deleted the feat/add-workflow-control-steps branch January 9, 2026 23:51
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