Skip to content

feat(workflow): add attach endpoint for in-progress streams#1084

Merged
omeraplak merged 1 commit into
mainfrom
feat/workflow-stream-attach-1081
Feb 19, 2026
Merged

feat(workflow): add attach endpoint for in-progress streams#1084
omeraplak merged 1 commit into
mainfrom
feat/workflow-stream-attach-1081

Conversation

@omeraplak

@omeraplak omeraplak commented Feb 19, 2026

Copy link
Copy Markdown
Member

PR Checklist

Please check if your PR fulfills the following requirements:

Bugs / Features

What is the current behavior?

  • Workflow streaming can only be established while starting a new execution via POST /workflows/:id/stream.
  • There is no API to attach to an already-running execution stream by executionId, so reconnect/resume observers cannot receive live events.

What is the new behavior?

  • Added GET /workflows/:id/executions/:executionId/stream to attach to an active workflow SSE stream.
  • Added replay support for reconnects using fromSequence query param and Last-Event-ID.
  • Added explicit attach error behavior:
    • 404 when workflow/execution is not found
    • 409 when execution is not streamable (completed/cancelled/error or no active stream session)
  • Kept POST /workflows/:id/stream behavior unchanged for starting new executions.
  • Resume now works with active streamed sessions (attach clients continue receiving events after resume).

fixes #1081

Notes for reviewers

  • Added new handler tests for stream attach behavior in packages/server-core/src/handlers/workflow.stream-attach.handlers.spec.ts.
  • Verified suspend -> attach -> resume flow manually against examples/with-workflow smoke scenario.

Summary by cubic

Adds an attach endpoint to join in-progress workflow SSE streams and support reconnects with replay. Addresses Linear #1081 and keeps the existing start-stream endpoint unchanged.

  • New Features
    • Added GET /workflows/:id/executions/:executionId/stream to attach to active streams.
    • Supports replay on reconnect via fromSequence query param and Last-Event-ID.
    • Returns 404 if workflow/execution not found, 409 if not streamable or no active session.
    • Resume now uses a fresh suspend controller so attached clients continue receiving events.
    • Added route handlers in server-core, Elysia, Hono, and serverless-Hono; protected by auth.
    • Updated OpenAPI route definitions and added handler tests.

Written for commit b425093. Summary will update on new commits.

Summary by CodeRabbit

Release Notes

  • New Features
    • Added ability to attach to in-progress workflow execution streams via Server-Sent Events (SSE) for real-time monitoring.
    • Implemented event replay on reconnection using Last-Event-ID header or fromSequence query parameter to recover previously missed stream events.
    • Existing workflow stream creation behavior remains unchanged.

@changeset-bot

changeset-bot Bot commented Feb 19, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b425093

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

This PR includes changesets to release 5 packages
Name Type
@voltagent/core Patch
@voltagent/server-core Patch
@voltagent/server-hono Patch
@voltagent/serverless-hono Patch
@voltagent/server-elysia 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

@ghost

This comment has been minimized.

@coderabbitai

coderabbitai Bot commented Feb 19, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds GET /workflows/:id/executions/:executionId/stream endpoint enabling clients to attach to active workflow execution streams with event replay support via Last-Event-ID or fromSequence parameters. Implements session-based streaming with event replay buffer, fresh suspend controller for resumed executions, and introduces handleAttachWorkflowStream handler across multiple server frameworks.

Changes

Cohort / File(s) Summary
Core Workflow Suspend Management
packages/core/src/workflow/core.ts
Introduces fresh suspend controller for resumed executions to isolate suspension state between original and resumed runs, preventing cross-contamination.
Authentication & Route Protection
packages/server-core/src/auth/defaults.ts, packages/server-core/src/auth/defaults.spec.ts
Adds new protected route GET /workflows/:id/executions/:executionId/stream to authenticated endpoints and verifies protection in test.
Route Definitions & Export
packages/server-core/src/routes/definitions.ts, packages/server-core/src/edge.ts
Defines new attachWorkflowStream route with 200/404/409/500 responses and re-exports handleAttachWorkflowStream handler for external use.
Stream Handler Implementation
packages/server-core/src/handlers/workflow.handlers.ts
Introduces persistent streaming session model with replay buffer, session lifecycle management (create/close/unregister), handleAttachWorkflowStream to attach to existing streams, and consumeWorkflowStream consumer loop to broadcast events to connected clients.
Handler Tests
packages/server-core/src/handlers/workflow.stream-attach.handlers.spec.ts
Comprehensive test suite covering 404/409 error cases, event replay from sequence cursor, and end-to-end flow across primary and attached streams.
Hono Framework Routes
packages/server-hono/src/routes/agent.routes.ts, packages/server-hono/src/routes/index.ts
Adds attachWorkflowStreamRoute definition and route handler extracting workflowId, executionId, query, and Last-Event-ID header to invoke handleAttachWorkflowStream with SSE response formatting.
Elysia Framework Route
packages/server-elysia/src/routes/workflow.routes.ts
Adds GET route at /workflows/:id/executions/:executionId/stream reading last-event-id header and invoking handleAttachWorkflowStream with appropriate SSE headers.
Serverless Hono Route
packages/serverless-hono/src/routes.ts
Adds attachWorkflowStream route following existing streaming pattern, handling workflowId/executionId extraction and Last-Event-ID header parsing.

Sequence Diagram

sequenceDiagram
    participant Client as Reconnecting Client
    participant Server as Server Handler
    participant SessionStore as Stream Session Store
    participant StreamExecution as Active Stream Execution
    participant SSEOutput as SSE Output

    Client->>Server: GET /workflows/:id/executions/:executionId/stream<br/>(Last-Event-ID or fromSequence)
    activate Server
    Server->>SessionStore: Lookup active stream session for executionId
    
    alt Session exists and active
        SessionStore-->>Server: Return active session with replay buffer
        Server->>StreamExecution: Validate execution state (running/suspended)
        
        alt Execution is streamable
            StreamExecution-->>Server: Confirm active stream
            Server->>SSEOutput: Create new stream from session replay buffer
            Server->>SSEOutput: Replay missed events (from Last-Event-ID/fromSequence onwards)
            SSEOutput-->>Client: Replay events (workflow-start, step-complete, etc.)
            
            loop Stream active
                StreamExecution->>SSEOutput: New event produced
                SSEOutput-->>Client: Stream event via SSE
            end
            
            Note over Client,SSEOutput: Client receives real-time events from sequence point
            
        else Execution is terminal
            Server-->>Client: 409 Conflict (execution not streamable)
        end
    else No active session
        Server-->>Client: 404 Not Found (workflow/execution not found)
    end
    deactivate Server
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Whiskers twitch at streaming dawn,
Clients reconnect, attachment drawn,
Sessions bloom with replay delight,
Fresh controllers keep events in flight,
In-progress flows now share their sight! 🌊✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.81% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(workflow): add attach endpoint for in-progress streams' directly summarizes the main change: adding a new attach endpoint for workflow streams.
Description check ✅ Passed The PR description fully completes the template with current/new behavior, checklist items marked, related issue linked, and notes for reviewers included.
Linked Issues check ✅ Passed All objectives from issue #1081 are met: GET /workflows/:id/executions/:executionId/stream endpoint implemented, replay support via fromSequence/Last-Event-ID added, explicit error responses (404/409) defined, POST endpoint behavior unchanged, resume works with active sessions, and route documented in OpenAPI definitions.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the attach endpoint feature: handler implementations, route definitions, server-specific route integrations, auth rules, tests, and changesets—all aligned with issue #1081 objectives.

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

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/workflow-stream-attach-1081

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

❤️ Share

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

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying voltagent with  Cloudflare Pages  Cloudflare Pages

Latest commit: b425093
Status: ✅  Deploy successful!
Preview URL: https://c2683baf.voltagent.pages.dev
Branch Preview URL: https://feat-workflow-stream-attach.voltagent.pages.dev

View logs

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 12 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


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

<violation number="1" location="packages/server-core/src/handlers/workflow.handlers.ts:772">
P1: Resumed stream events are not broadcast to subscribers. After `activeSession.streamExecution = resumedStreamExecution`, the original `consumeWorkflowStream` loop (started at stream creation) is still iterating the *old* async iterable — `for await...of` captures the iterator once. If that iterator ended when the workflow suspended, no consumer is iterating the new `resumedStreamExecution` to call `broadcastWorkflowStreamEvent`. Consider re-invoking `consumeWorkflowStream(activeSession, deps, logger)` after updating `streamExecution` so resumed events reach attached clients.</violation>
</file>

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

resumeData,
options?.stepId ? { stepId: options.stepId } : undefined,
);
activeSession.streamExecution = resumedStreamExecution;

@cubic-dev-ai cubic-dev-ai Bot Feb 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Resumed stream events are not broadcast to subscribers. After activeSession.streamExecution = resumedStreamExecution, the original consumeWorkflowStream loop (started at stream creation) is still iterating the old async iterable — for await...of captures the iterator once. If that iterator ended when the workflow suspended, no consumer is iterating the new resumedStreamExecution to call broadcastWorkflowStreamEvent. Consider re-invoking consumeWorkflowStream(activeSession, deps, logger) after updating streamExecution so resumed events reach attached clients.

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

<comment>Resumed stream events are not broadcast to subscribers. After `activeSession.streamExecution = resumedStreamExecution`, the original `consumeWorkflowStream` loop (started at stream creation) is still iterating the *old* async iterable — `for await...of` captures the iterator once. If that iterator ended when the workflow suspended, no consumer is iterating the new `resumedStreamExecution` to call `broadcastWorkflowStreamEvent`. Consider re-invoking `consumeWorkflowStream(activeSession, deps, logger)` after updating `streamExecution` so resumed events reach attached clients.</comment>

<file context>
@@ -485,6 +759,37 @@ export async function handleResumeWorkflow(
+        resumeData,
+        options?.stepId ? { stepId: options.stepId } : undefined,
+      );
+      activeSession.streamExecution = resumedStreamExecution;
+
+      const status = await resumedStreamExecution.status;
</file context>
Fix with Cubic

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
packages/core/src/workflow/core.ts (1)

2229-2238: ⚠️ Potential issue | 🟡 Minor

Missing lastEventSequence in resumeFrom causes event sequence counter to reset on every resume, breaking replay for re-suspended executions.

executionContext.eventSequence is initialized at Line 974 as options?.resumeFrom?.lastEventSequence || 0. The resumeFrom object here does not forward lastEventSequence from execResult.suspension, so the resumed execution always starts its event sequence counter from 0. If the resumed workflow suspends a second time, the stored lastEventSequence in memory will be 0 — not a continuation of the sequence from the initial run.

The attach endpoint's replay buffer relies on this stored lastEventSequence as the fromSequence baseline when a client re-attaches. Clients attaching after a second suspension will receive a stale/incorrect sequence starting position.

🛠️ Proposed fix
 const resumeOptions: WorkflowRunOptions = {
   executionId: execResult.executionId,
   resumeFrom: {
     executionId: execResult.executionId,
     checkpoint: execResult.suspension.checkpoint,
     resumeStepIndex,
     resumeData: input,
+    lastEventSequence: execResult.suspension.lastEventSequence,
   },
   suspendController: resumedSuspendController,
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/workflow/core.ts` around lines 2229 - 2238, The
resumeOptions.resumeFrom object is missing lastEventSequence so resumed runs
restart their eventSequence at 0; update the resumeOptions construction (the
resumeOptions constant) to include resumeFrom.lastEventSequence set from
execResult.suspension.lastEventSequence (or the appropriate field on
execResult.suspension) so WorkflowRunOptions.resumeFrom carries
lastEventSequence into subsequent resumes; keep the rest of the fields
(executionId, checkpoint, resumeStepIndex, resumeData, suspendController)
unchanged and ensure the property name is exactly lastEventSequence to match
executionContext's resume handling.
🧹 Nitpick comments (4)
packages/server-core/src/handlers/workflow.stream-attach.handlers.spec.ts (2)

175-262: Solid integration test for attach + replay flow.

The promise-based gating pattern (releaseSecondEvent) keeps the test deterministic. One gap: only fromSequence replay is tested — the lastEventId path (which the handler also supports via parseReplaySequence) is not covered. Consider adding a small variant.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server-core/src/handlers/workflow.stream-attach.handlers.spec.ts`
around lines 175 - 262, Add a second test variant that exercises the alternate
replay path using lastEventId: duplicate the existing "attaches to active stream
and replays from sequence cursor" test but call handleAttachWorkflowStream with
a payload using lastEventId (set to firstEvent.id) instead of fromSequence, or
explicitly invoke parseReplaySequence if the handler expects that form; ensure
you create the same deterministic gating (releaseSecondEvent) and assert the
same sequence of events from both primaryReader and attachedReader
(step-complete and workflow-result) to validate the lastEventId replay path in
handleAttachWorkflowStream/parseReplaySequence.

84-115: readSSEEvent assumes one SSE event per read() call.

This helper will break if the stream implementation ever batches multiple events into a single chunk or splits an event across chunks. For unit tests against the in-memory stream factory this is fine, but consider adding a brief comment documenting this assumption for future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server-core/src/handlers/workflow.stream-attach.handlers.spec.ts`
around lines 84 - 115, The helper readSSEEvent assumes each reader.read() yields
exactly one complete SSE event and will break if chunks contain multiple events
or split an event across reads; add a concise comment above the readSSEEvent
function stating this explicit assumption (and its caveat about
batching/splitting), and optionally mention that a more robust implementation
would accumulate chunks and parse by SSE event boundaries if needed in the
future.
packages/server-core/src/handlers/workflow.handlers.ts (2)

36-36: Module-level singleton activeWorkflowStreamSessions won't work across multiple server instances.

This in-memory Map is inherently single-process. In serverless or horizontally-scaled deployments, attach requests hitting a different instance won't find the session. This is likely acceptable for server-core's intended local-dev / single-process use case, but it's worth noting as a limitation in documentation or a code comment.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server-core/src/handlers/workflow.handlers.ts` at line 36, The
module-level Map activeWorkflowStreamSessions is a single-process in-memory
store and will not work across multiple server instances; update the code around
the declaration of activeWorkflowStreamSessions to either (a) add a clear
comment/documentation note that it is intentionally single-process for local/dev
use and list the limitation, or (b) replace it with a cross-instance backing
store (e.g., Redis) and update the access points that use
activeWorkflowStreamSessions (look for functions handling session
create/attach/lookup in workflow.handlers.ts) to use the shared store API
(SET/GET/DEL or client wrapper) instead so sessions are discoverable across
instances. Ensure the chosen approach updates all reads/writes to
activeWorkflowStreamSessions to use the new store or includes the explanatory
comment near the Map declaration.

762-791: Resume handler blocks until workflow completion when a streaming session is active.

When an active streaming session exists, handleResumeWorkflow awaits resumedStreamExecution.status, .result, and .endAt (Lines 774-776). For long-running workflows, this keeps the HTTP response pending until the entire workflow finishes. Meanwhile, consumeWorkflowStream is also running and will attempt to read the same terminal promises.

This may be intentional (the caller gets the final result synchronously), but for workflows that take minutes/hours after resume, the HTTP request will likely time out. Consider returning an immediate acknowledgement (e.g., { status: "resumed", executionId }) and letting clients observe completion via the attached SSE stream instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server-core/src/handlers/workflow.handlers.ts` around lines 762 -
791, The resume branch currently awaits terminal promises
(resumedStreamExecution.status, .result, .endAt) which blocks the HTTP response
until workflow completion; instead, after calling
activeSession.streamExecution.resume(...) and updating
activeSession.streamExecution, return an immediate acknowledgement (e.g., {
success: true, data: { executionId: resumedStreamExecution.executionId, status:
"resumed", startAt: resumedStreamExecution.startAt instanceof Date ?
resumedStreamExecution.startAt.toISOString() : resumedStreamExecution.startAt }
}) without awaiting status/result/endAt so the request doesn't hang; keep the
call to activeSession.streamExecution = resumedStreamExecution and let clients
consume completion via the existing SSE stream/consumeWorkflowStream.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/core/src/workflow/core.ts`:
- Around line 2229-2238: The resumeOptions.resumeFrom object is missing
lastEventSequence so resumed runs restart their eventSequence at 0; update the
resumeOptions construction (the resumeOptions constant) to include
resumeFrom.lastEventSequence set from execResult.suspension.lastEventSequence
(or the appropriate field on execResult.suspension) so
WorkflowRunOptions.resumeFrom carries lastEventSequence into subsequent resumes;
keep the rest of the fields (executionId, checkpoint, resumeStepIndex,
resumeData, suspendController) unchanged and ensure the property name is exactly
lastEventSequence to match executionContext's resume handling.

---

Nitpick comments:
In `@packages/server-core/src/handlers/workflow.handlers.ts`:
- Line 36: The module-level Map activeWorkflowStreamSessions is a single-process
in-memory store and will not work across multiple server instances; update the
code around the declaration of activeWorkflowStreamSessions to either (a) add a
clear comment/documentation note that it is intentionally single-process for
local/dev use and list the limitation, or (b) replace it with a cross-instance
backing store (e.g., Redis) and update the access points that use
activeWorkflowStreamSessions (look for functions handling session
create/attach/lookup in workflow.handlers.ts) to use the shared store API
(SET/GET/DEL or client wrapper) instead so sessions are discoverable across
instances. Ensure the chosen approach updates all reads/writes to
activeWorkflowStreamSessions to use the new store or includes the explanatory
comment near the Map declaration.
- Around line 762-791: The resume branch currently awaits terminal promises
(resumedStreamExecution.status, .result, .endAt) which blocks the HTTP response
until workflow completion; instead, after calling
activeSession.streamExecution.resume(...) and updating
activeSession.streamExecution, return an immediate acknowledgement (e.g., {
success: true, data: { executionId: resumedStreamExecution.executionId, status:
"resumed", startAt: resumedStreamExecution.startAt instanceof Date ?
resumedStreamExecution.startAt.toISOString() : resumedStreamExecution.startAt }
}) without awaiting status/result/endAt so the request doesn't hang; keep the
call to activeSession.streamExecution = resumedStreamExecution and let clients
consume completion via the existing SSE stream/consumeWorkflowStream.

In `@packages/server-core/src/handlers/workflow.stream-attach.handlers.spec.ts`:
- Around line 175-262: Add a second test variant that exercises the alternate
replay path using lastEventId: duplicate the existing "attaches to active stream
and replays from sequence cursor" test but call handleAttachWorkflowStream with
a payload using lastEventId (set to firstEvent.id) instead of fromSequence, or
explicitly invoke parseReplaySequence if the handler expects that form; ensure
you create the same deterministic gating (releaseSecondEvent) and assert the
same sequence of events from both primaryReader and attachedReader
(step-complete and workflow-result) to validate the lastEventId replay path in
handleAttachWorkflowStream/parseReplaySequence.
- Around line 84-115: The helper readSSEEvent assumes each reader.read() yields
exactly one complete SSE event and will break if chunks contain multiple events
or split an event across reads; add a concise comment above the readSSEEvent
function stating this explicit assumption (and its caveat about
batching/splitting), and optionally mention that a more robust implementation
would accumulate chunks and parse by SSE event boundaries if needed in the
future.

@omeraplak
omeraplak merged commit 95ad610 into main Feb 19, 2026
23 checks passed
@omeraplak
omeraplak deleted the feat/workflow-stream-attach-1081 branch February 19, 2026 20:29
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.

[FEAT] Add execution attach endpoint for in-progress workflow streams

1 participant