Skip to content

fix(client): scope workflow global patching to execution context#213

Draft
robelest wants to merge 2 commits intoget-convex:mainfrom
robelest:robel/workflow-scoped-patching
Draft

fix(client): scope workflow global patching to execution context#213
robelest wants to merge 2 commits intoget-convex:mainfrom
robelest:robel/workflow-scoped-patching

Conversation

@robelest
Copy link

@robelest robelest commented Mar 1, 2026

Summary

  • Scope workflow global patching to execution context instead of mutating globals process-wide.
  • Run workflow handler/executor under runWithWorkflowEnvironment(...) so deterministic Math/Date and blocked fetch/timers only apply while a workflow is executing.
  • Add regression coverage for passthrough behavior outside workflow execution and async-boundary behavior inside workflow execution.

Dependency

Validation

  • bun run lint
  • bun run build
  • bun run test
  • Local backend smoke checks against convex-backend feature/async-hooks-cped-runtime:
    • internal.example.startWorkflow -> completed via api.admin.getWorkflowStatus and api.admin.getWorkflowResult
    • internal.example.getGeocoding succeeds outside workflow scope
    • internal.transcription.computeEmbedding succeeds when OPENAI_API_KEY is configured

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Summary by CodeRabbit

  • Bug Fixes
    • Improved workflow environment isolation to prevent unintended global state changes during execution.
    • Restricted certain operations (setTimeout, setInterval, fetch, Date.now) within workflows to ensure deterministic behavior.
    • Fixed global state restoration after workflow completion.

Route Math/Date/console/fetch/timer patches through AsyncLocalStorage so they only apply during workflow execution, preventing global leakage into non-workflow code while preserving deterministic workflow behavior.
@coderabbitai
Copy link

coderabbitai bot commented Mar 1, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The environment patching system is refactored from direct global patching to a workflow-aware AsyncLocalStorage-backed architecture. Global patching now happens lazily via per-workflow environments, the setupEnvironment function signature is simplified, and a new runWithWorkflowEnvironment function orchestrates execution within a workflow context.

Changes

Cohort / File(s) Summary
Environment Infrastructure
src/client/environment.ts
Reworked patching to use AsyncLocalStorage-backed per-workflow environments. Replaced direct global patches with lazy accessors. setupEnvironment signature changed (no parameters), new runWithWorkflowEnvironment added for orchestration. Introduces createWorkflowEnvironment and defineWorkflowAwareGlobal helper utilities.
Environment Tests
src/client/environment.test.ts
Added workflow-scoped patching tests verifying isolation of global changes, restricted operations within workflows (setTimeout, setInterval, fetch, Date.now), and proper restoration of originals. Tests AsyncLocalStorage shimming and dual context behavior.
Integration
src/client/workflowMutation.ts
Updated to use new runWithWorkflowEnvironment orchestration API instead of direct setupEnvironment call, wrapping handler/executor race within workflow context.

Sequence Diagram

sequenceDiagram
    participant Client
    participant WorkflowMutation
    participant RunWithEnv as runWithWorkflowEnvironment
    participant Storage as AsyncLocalStorage
    participant WorkflowEnv as WorkflowEnvironment
    participant GlobalAccessor as Global Accessor

    Client->>WorkflowMutation: execute workflow
    WorkflowMutation->>RunWithEnv: runWithWorkflowEnvironment(getGenerationState, workflowId, run)
    RunWithEnv->>WorkflowEnv: createWorkflowEnvironment(getGenerationState, workflowId)
    WorkflowEnv-->>RunWithEnv: patched Math, Date, console, etc.
    RunWithEnv->>Storage: store.run(WorkflowEnv, () => ...)
    Storage->>GlobalAccessor: within AsyncLocalStorage context
    GlobalAccessor->>WorkflowEnv: access patched globals
    WorkflowEnv-->>GlobalAccessor: deterministic/restricted values
    GlobalAccessor->>Client: results with isolated environment
    Storage->>Storage: cleanup context
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • ianmacartney

Poem

🐰 Async storage now holds our dreams,
Workflows wrapped in context streams,
Globals lazy, patched with care,
Environment in the air!
No more direct patching's song—
Workflow-aware, pure and strong!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(client): scope workflow global patching to execution context' clearly and specifically describes the main change: restricting global patches to workflow execution rather than process-wide, which is the core architectural shift in this PR.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@robelest
Copy link
Author

robelest commented Mar 2, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Mar 2, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@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

🧹 Nitpick comments (1)
src/client/environment.test.ts (1)

11-17: Consider adding a comment explaining the polyfill purpose.

The conditional polyfill ensures AsyncLocalStorage is available on globalThis for the test environment, matching the runtime expectation in ensureWorkflowEnvironmentStorage(). A brief comment would clarify this setup for future maintainers.

📝 Suggested documentation
+// The workflow environment expects AsyncLocalStorage on globalThis (as provided by
+// convex-backend). Polyfill it for the test environment.
 if (
   (globalThis as { AsyncLocalStorage?: typeof AsyncLocalStorage })
     .AsyncLocalStorage === undefined
 ) {
   (globalThis as { AsyncLocalStorage?: typeof AsyncLocalStorage }).AsyncLocalStorage =
     AsyncLocalStorage;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/environment.test.ts` around lines 11 - 17, Add a short inline
comment above the polyfill block explaining that this conditional assigns
AsyncLocalStorage onto globalThis for the test environment to mirror runtime
expectations used by ensureWorkflowEnvironmentStorage(), so future maintainers
understand it’s a deliberate test-only polyfill and not accidental global
mutation; reference the AsyncLocalStorage symbol and
ensureWorkflowEnvironmentStorage() in the comment for context.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/client/environment.ts`:
- Around line 164-177: createWorkflowEnvironment currently reads globals
(global.Math, global.Date, global.console) which can be getters after
setupEnvironment runs and yield already-patched objects in nested workflow
contexts; fix this by capturing the original unpatched globals once (e.g., a
module-level ORIGINALS object populated from globalThis.Math, globalThis.Date,
globalThis.console before setupEnvironment patches anything) and change
createWorkflowEnvironment to call patchMath, createDeterministicDate, and
createConsole using those ORIGINALS (reference ORIGINALS.Math, ORIGINALS.Date,
ORIGINALS.console) instead of reading from globalThis; ensure the capture
happens before setupEnvironment runs so patchMath, createDeterministicDate, and
createConsole always wrap the true originals.

---

Nitpick comments:
In `@src/client/environment.test.ts`:
- Around line 11-17: Add a short inline comment above the polyfill block
explaining that this conditional assigns AsyncLocalStorage onto globalThis for
the test environment to mirror runtime expectations used by
ensureWorkflowEnvironmentStorage(), so future maintainers understand it’s a
deliberate test-only polyfill and not accidental global mutation; reference the
AsyncLocalStorage symbol and ensureWorkflowEnvironmentStorage() in the comment
for context.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b1552ab and 48b534c.

📒 Files selected for processing (3)
  • src/client/environment.test.ts
  • src/client/environment.ts
  • src/client/workflowMutation.ts

"AsyncLocalStorage is not available in this runtime. Update convex-backend to a build with async_hooks support.",
);
}
workflowEnvironmentStorage = new global.AsyncLocalStorage<WorkflowEnvironment>();
Copy link
Member

Choose a reason for hiding this comment

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

why using the global instead of an import?

Copy link
Author

@robelest robelest Mar 3, 2026

Choose a reason for hiding this comment

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

I kept the global path because it gives deterministic diagnostics (explicit availability check + clear error). The current node:async_hooks shim also has permissive fallbacks (createHook no-op, IDs 0) when async_hooks is absent, so it can fail less explicitly. Happy to follow up by tightening shim error behavior so import-path diagnostics match the global-path check

Copy link
Member

Choose a reason for hiding this comment

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

Gotcha. Will this work in convex-test where our v8 shim doesn’t run? I don’t know if it’s normally a global or if we’re special-casing our own behavior

type AsyncLocalStorageConstructor = new <T>() => AsyncLocalStorageLike<T>;

let workflowEnvironmentStorage: AsyncLocalStorageLike<WorkflowEnvironment> | undefined;
let globalsPatched = false;
Copy link
Member

Choose a reason for hiding this comment

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

why isn't this !!workflowEnvironmentsStorage? What's different?

Copy link
Author

Choose a reason for hiding this comment

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

Storage creation and global patching are separate phases. globalsPatched makes that boundary explicit and avoids conflating partial setup with done.

…ment

- Capture Math/Date/console into module-level originalGlobals before
  setupEnvironment runs so createWorkflowEnvironment always wraps the
  true originals, not getters that may return an outer workflow's patched
  objects in a nested execution context (CodeRabbit feedback).
- Add comment explaining the globalThis.AsyncLocalStorage polyfill in
  environment.test.ts (CodeRabbit nitpick).
@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 3, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@convex-dev/workflow@213

commit: b464856

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